Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"react-dom": "^19.2.6",
"react-hook-form": "^7.80.0",
"react-router-dom": "^7.18.0",
"react-toastify": "^11.1.0",
"recharts": "^3.9.2",
"rrule": "^2.8.1",
"shadcn": "^4.11.0",
Expand Down
15 changes: 0 additions & 15 deletions apps/admin/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions apps/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import EscalationPoliciesPage from "@/features/escalation/pages/EscalationPolici
import SubscriptionsPage from "@/features/notification-subscriptions/pages/SubscriptionsPage";
import EscalationPolicyDetailPage from "@/features/escalation/pages/EscalationPolicyDetailPage";
import ProfilePage from "@/features/profile/pages/ProfilePage";
import { ToastContainer } from 'react-toastify';
import { Toaster } from "@/components/ui/sonner";

const queryClient = new QueryClient({
Expand All @@ -88,7 +87,6 @@ function ProtectedLayout() {
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<ToastContainer />
<TooltipProvider>
<ThemeProvider>
<Toaster />
Expand Down
6 changes: 1 addition & 5 deletions apps/admin/src/components/AdminLayout/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ import { useAuth } from "@/hooks/useAuth";
import { useHealth } from "@/hooks/useHealth";
import { useTimezone } from "@/hooks/useTimezone";
import { useMyOnCallCurrentStatus } from "@/hooks/useOnCallMe";
import { useOnCallNowDismissal } from "@/hooks/useOnCallNowDismissal";
import { useTimezoneMismatchDismissal } from "@/hooks/useTimezoneMismatchDismissal";
import { TimezoneMismatchBanner } from "@/components/TimezoneMismatchBanner";
import { OnCallNowBanner } from "@/components/OnCallNowBanner";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { GlobalSearchDialog } from "@/components/GlobalSearchDialog";
import { siteApi, maintenancesApi } from "@/lib/api";
Expand Down Expand Up @@ -346,8 +344,7 @@ export function AdminLayout({ children }: AdminLayoutProps) {
const { resolvedTheme, setTheme } = useTheme();
const { mismatch, useBrowserTimeZone, activeTimeZone, profileTimeZone, browserTimeZone } = useTimezone();
const { data: currentOnCallSlot } = useMyOnCallCurrentStatus();
const { isDismissed: onCallBannerDismissed } = useOnCallNowDismissal(currentOnCallSlot);
const showOnCallIcon = !!currentOnCallSlot && onCallBannerDismissed;
const showOnCallIcon = !!currentOnCallSlot;
const { isDismissed: timezoneMismatchDismissed } = useTimezoneMismatchDismissal(
profileTimeZone ?? "",
browserTimeZone
Expand Down Expand Up @@ -496,7 +493,6 @@ export function AdminLayout({ children }: AdminLayoutProps) {
</header>

<TimezoneMismatchBanner />
<OnCallNowBanner />

{/* Page content */}
<main className="flex-1 overflow-y-auto p-6">{children}</main>
Expand Down
44 changes: 0 additions & 44 deletions apps/admin/src/components/OnCallNowBanner.tsx

This file was deleted.

19 changes: 19 additions & 0 deletions apps/admin/src/components/PageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cn } from "@/lib/utils";

interface Props {
children: React.ReactNode;
className?: string;
}

/**
* Top-level wrapper for a page's content. Sits directly inside the layout's
* scrollable <main> (which already provides page padding), so this only owns
* the page's own width/flow — not outer spacing. Use it instead of a bare
* <div> at the root of a page component.
*/
function PageContainer(props: Props) {
const { children, className } = props;
return <div className={cn("w-full", className)}>{children}</div>;
}

export default PageContainer;
27 changes: 27 additions & 0 deletions apps/admin/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"

import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"

function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}

export { Checkbox }
36 changes: 36 additions & 0 deletions apps/admin/src/components/ui/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"

import { cn } from "@/lib/utils"

function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
className={cn("grid w-full gap-2", className)}
{...props}
/>
)
}

function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<RadioPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
)
}

export { RadioGroup, RadioGroupItem }
16 changes: 16 additions & 0 deletions apps/admin/src/features/alerts/components/AlertField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface Props {
label: string;
children: React.ReactNode;
}

function AlertField(props: Props) {
const { label, children } = props;
return (
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
<div className="text-sm">{children}</div>
</div>
);
}

export default AlertField;
51 changes: 51 additions & 0 deletions apps/admin/src/features/alerts/components/AlertIncidentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ExternalLink, Link2, PlusCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { AlertDetail } from "@/lib/actions/alerts";

interface Props {
alert: AlertDetail;
isCreating: boolean;
onViewIncident: (incidentId: number) => void;
onAttach: () => void;
onCreate: () => void;
}

function AlertIncidentSection(props: Props) {
const { alert, isCreating, onViewIncident, onAttach, onCreate } = props;

if (alert.incidentId != null) {
return (
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-semibold">Linked Incident</p>
<p className="text-xs text-muted-foreground mt-0.5">{alert.incidentTitle ?? `Incident #${alert.incidentId}`}</p>
</div>
<Button variant="outline" onClick={() => onViewIncident(alert.incidentId!)}>
<ExternalLink size={14} />
View incident
</Button>
</div>
);
}

return (
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-semibold">Not linked</p>
<p className="text-xs text-muted-foreground mt-0.5">This alert isn't linked to any incident yet.</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={onAttach}>
<Link2 size={14} />
Attach to incident
</Button>
<Button onClick={onCreate} disabled={isCreating}>
<PlusCircle size={14} />
{isCreating ? "Creating…" : "Create incident"}
</Button>
</div>
</div>
);
}

export default AlertIncidentSection;
91 changes: 91 additions & 0 deletions apps/admin/src/features/alerts/components/AlertOverviewSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Link } from "react-router-dom";
import { ExternalLink, FileJson } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useFormattedDate } from "@/hooks/useFormattedDate";
import { ROUTES } from "@/constants/routes";
import type { AlertDetail } from "@/lib/actions/alerts";
import { AlertSourceBadge } from "./AlertSourceBadge";
import AlertField from "./AlertField";

interface Props {
alert: AlertDetail;
onViewPayload: () => void;
}

function AlertOverviewSection(props: Props) {
const { alert, onViewPayload } = props;
const { formatDateTimeOrDash } = useFormattedDate();

return (
<div className="flex flex-col gap-5">
{alert.source !== "Internal" ? (
<AlertField label="Source">
<div className="flex items-center gap-3">
<AlertSourceBadge source={alert.source} sourceLabel={alert.sourceLabel} sourceIconifyIcon={alert.sourceIconifyIcon} />
{alert.sourceUrl && (
<a
href={alert.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
>
View in {alert.sourceLabel ?? "source"} <ExternalLink size={12} />
</a>
)}
</div>
</AlertField>
) : (
<div className="grid grid-cols-2 gap-5">
<AlertField label="Check">
{alert.checkSlug && alert.serviceSlug ? (
<Link to={ROUTES.CHECKS.DETAIL(alert.serviceSlug, alert.checkSlug)} className="font-semibold hover:underline">
{alert.checkName}
</Link>
) : (
<span className="text-muted-foreground">—</span>
)}
</AlertField>
<AlertField label="Service">
{alert.serviceSlug ? (
<Link to={ROUTES.SERVICES.DETAIL(alert.serviceSlug)} className="font-semibold hover:underline">
{alert.serviceName}
</Link>
) : (
<span className="text-muted-foreground">—</span>
)}
</AlertField>
</div>
)}

<div className="grid grid-cols-2 gap-5">
<AlertField label="Fired At">{formatDateTimeOrDash(alert.firedAt)}</AlertField>
<AlertField label="Resolved At">
{alert.resolvedAt ? formatDateTimeOrDash(alert.resolvedAt) : <span className="text-red-600 font-medium">Active</span>}
</AlertField>
</div>

<div className="grid grid-cols-2 gap-5">
<AlertField label="Occurrences">{alert.occurrenceCount}</AlertField>
<AlertField label="Severity">{alert.severity ?? "—"}</AlertField>
</div>

<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">Message</span>
<p className="text-sm rounded-lg border bg-muted/30 px-3 py-2 font-mono">
{alert.message ?? "—"}
</p>
</div>

{alert.sourceRawPayload && (
<div>
<Button type="button" variant="outline" size="sm" onClick={onViewPayload}>
<FileJson size={13} />
View raw payload
</Button>
</div>
)}
</div>
);
}

export default AlertOverviewSection;
Loading
Loading