diff --git a/messages/en-US.json b/messages/en-US.json index 3daef96e4..26fbe5572 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2953,9 +2953,9 @@ "streamingHttpWebhookTitle": "HTTP Webhook", "streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.", "streamingS3Title": "Amazon S3", - "streamingS3Description": "Stream events to an S3-compatible object storage bucket. Contact support to enable this destination.", + "streamingS3Description": "Stream events to an S3-compatible object storage bucket.", "streamingDatadogTitle": "Datadog", - "streamingDatadogDescription": "Forward events directly to your Datadog account. Contact support to enable this destination.", + "streamingDatadogDescription": "Forward events directly to your Datadog account.", "streamingTypePickerDescription": "Choose a destination type to get started.", "streamingFailedToLoad": "Failed to load destinations", "streamingUnexpectedError": "An unexpected error occurred.", @@ -3031,5 +3031,9 @@ "httpDestCreateFailed": "Failed to create destination", "followRedirects": "Follow Redirects", "followRedirectsDescription": "Automatically follow HTTP redirects for requests.", - "alertingErrorWebhookUrl": "Please enter a valid URL for the webhook." + "alertingErrorWebhookUrl": "Please enter a valid URL for the webhook.", + "healthCheckStrategyHttp": "Validates connectivity and checks the HTTP response status.", + "healthCheckStrategyTcp": "Verifies TCP connectivity only, without inspecting the response.", + "healthCheckStrategySnmp": "Makes an SNMP get request to check the health of network devices and infrastructure.", + "healthCheckStrategyIcmp": "Uses ICMP echo requests (pings) to check if a resource is reachable and responsive." } diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index d64ed1b56..5ae57c8a7 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -21,7 +21,9 @@ export enum TierFeature { SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed SIEM = "siem", // handle downgrade by disabling SIEM integrations HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources - DomainNamespaces = "domainNamespaces" // handle downgrade by removing custom domain namespaces + DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces + StandaloneHealthChecks = "standaloneHealthChecks", + AlertingRules = "alertingRules" } export const tierMatrix: Record = { @@ -60,5 +62,7 @@ export const tierMatrix: Record = { [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"], [TierFeature.SIEM]: ["enterprise"], [TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"], - [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"] + [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.StandaloneHealthChecks]: ["tier2", "tier3", "enterprise"], + [TierFeature.AlertingRules]: ["tier2", "tier3", "enterprise"] }; diff --git a/src/app/[orgId]/settings/alerting/[ruleId]/page.tsx b/src/app/[orgId]/settings/alerting/[ruleId]/page.tsx index c9ef938d5..34afceaab 100644 --- a/src/app/[orgId]/settings/alerting/[ruleId]/page.tsx +++ b/src/app/[orgId]/settings/alerting/[ruleId]/page.tsx @@ -4,7 +4,9 @@ import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGra import { apiResponseToFormValues } from "@app/lib/alertRuleForm"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; @@ -21,6 +23,8 @@ export default function EditAlertRulePage() { const alertRuleId = parseInt(ruleIdParam, 10); const api = createApiClient(useEnvContext()); + const { isPaidUser } = usePaidStatus(); + const isPaid = isPaidUser(tierMatrix.alertingRules); const [formValues, setFormValues] = useState(undefined); @@ -73,6 +77,7 @@ export default function EditAlertRulePage() { alertRuleId={alertRuleId} initialValues={formValues} isNew={false} + disabled={!isPaid} /> ); } diff --git a/src/app/[orgId]/settings/alerting/create/page.tsx b/src/app/[orgId]/settings/alerting/create/page.tsx index fc5c51660..babc018fa 100644 --- a/src/app/[orgId]/settings/alerting/create/page.tsx +++ b/src/app/[orgId]/settings/alerting/create/page.tsx @@ -2,17 +2,22 @@ import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor"; import { defaultFormValues } from "@app/lib/alertRuleForm"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { useParams } from "next/navigation"; export default function NewAlertRulePage() { const params = useParams(); const orgId = params.orgId as string; + const { isPaidUser } = usePaidStatus(); + const isPaid = isPaidUser(tierMatrix.alertingRules); return ( ); } \ No newline at end of file diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx index 5192a9c9b..069059868 100644 --- a/src/app/[orgId]/settings/logs/streaming/page.tsx +++ b/src/app/[orgId]/settings/logs/streaming/page.tsx @@ -22,7 +22,8 @@ import { } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; import { Switch } from "@app/components/ui/switch"; -import { Globe, MoreHorizontal, Plus } from "lucide-react"; +import { Globe, MoreHorizontal, Plus, ExternalLink, KeyRound } from "lucide-react"; +import Link from "next/link"; import { AxiosResponse } from "axios"; import { build } from "@server/build"; import Image from "next/image"; @@ -181,6 +182,65 @@ interface DestinationTypePickerProps { isPaywalled?: boolean; } +const BOOK_A_DEMO_URL = "https://click.fossorial.io/ep922"; +const CONTACT_URL = "https://pangolin.net/contact"; + +function ContactSalesDialog({ + open, + onOpenChange +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const t = useTranslations(); + return ( + + + + {t("streamingAddDestination")} + + +
+
+
+ + + Contact sales to enable this feature.{" "} + + Book a demo + + + {" or "} + + contact us + + + . + +
+
+
+
+ + + + + +
+
+ ); +} + function DestinationTypePicker({ open, onOpenChange, @@ -189,6 +249,17 @@ function DestinationTypePicker({ }: DestinationTypePickerProps) { const t = useTranslations(); const [selected, setSelected] = useState("http"); + const [contactSalesOpen, setContactSalesOpen] = useState(false); + + const ENTERPRISE_ONLY_TYPES: DestinationType[] = ["s3", "datadog"]; + + function handleOptionSelect(type: DestinationType) { + if (ENTERPRISE_ONLY_TYPES.includes(type)) { + setContactSalesOpen(true); + } else { + onSelect(type); + } + } const destinationTypeOptions: ReadonlyArray< StrategyOption @@ -203,7 +274,6 @@ function DestinationTypePicker({ id: "s3", title: t("streamingS3Title"), description: t("streamingS3Description"), - disabled: true, icon: ( + @@ -255,7 +329,12 @@ function DestinationTypePicker({ { + setSelected(type); + if (ENTERPRISE_ONLY_TYPES.includes(type)) { + setContactSalesOpen(true); + } + }} cols={1} /> @@ -265,7 +344,7 @@ function DestinationTypePicker({ + + + + + ); +} type HealthCheckFormFieldsProps = { form: UseFormReturn; @@ -40,10 +112,19 @@ export function HealthCheckFormFields({ watchedMode }: HealthCheckFormFieldsProps) { const t = useTranslations(); + const [contactSalesOpen, setContactSalesOpen] = useState(false); const showFields = hideEnabledField || watchedEnabled; - const handleChange = (fieldName: string, value: any, fieldOnChange: (v: any) => void) => { + const handleChange = ( + fieldName: string, + value: any, + fieldOnChange: (v: any) => void + ) => { + if (fieldName === "hcMode" && UNIMPLEMENTED_MODES.includes(value)) { + setContactSalesOpen(true); + return; + } fieldOnChange(value); if (onFieldChange) { onFieldChange(fieldName, value); @@ -52,6 +133,10 @@ export function HealthCheckFormFields({ return ( <> + {/* Name */} {showNameField && ( @@ -86,7 +173,11 @@ export function HealthCheckFormFields({ - handleChange("hcEnabled", value, field.onChange) + handleChange( + "hcEnabled", + value, + field.onChange + ) } /> @@ -103,25 +194,41 @@ export function HealthCheckFormFields({ name="hcMode" render={({ field }) => ( - {t("healthCheckStrategy")} + + {t("healthCheckStrategy")} + - handleChange("hcMode", value, field.onChange) + handleChange( + "hcMode", + value, + field.onChange + ) } /> @@ -138,7 +245,9 @@ export function HealthCheckFormFields({ name="hcHostname" render={({ field }) => ( - {t("healthHostname")} + + {t("healthHostname")} + { - const value = e.target.value; - handleChange("hcPort", value, field.onChange); + const value = + e.target.value; + handleChange( + "hcPort", + value, + field.onChange + ); }} /> @@ -185,23 +299,35 @@ export function HealthCheckFormFields({ name="hcScheme" render={({ field }) => ( - {t("healthScheme")} + + {t("healthScheme")} + @@ -213,7 +339,9 @@ export function HealthCheckFormFields({ name="hcHostname" render={({ field }) => ( - {t("healthHostname")} + + {t("healthHostname")} + { - const value = e.target.value; - handleChange("hcPort", value, field.onChange); + const value = + e.target.value; + handleChange( + "hcPort", + value, + field.onChange + ); }} /> @@ -266,23 +399,39 @@ export function HealthCheckFormFields({ {t("httpMethod")} @@ -294,7 +443,9 @@ export function HealthCheckFormFields({ name="hcPath" render={({ field }) => ( - {t("healthCheckPath")} + + {t("healthCheckPath")} + ( - {t("timeoutSeconds")} + + {t("timeoutSeconds")} + { - const value = parseInt(e.target.value); - handleChange("hcTimeout", value, field.onChange); + const value = parseInt( + e.target.value + ); + handleChange( + "hcTimeout", + value, + field.onChange + ); }} /> @@ -347,8 +506,14 @@ export function HealthCheckFormFields({ type="number" {...field} onChange={(e) => { - const value = parseInt(e.target.value); - handleChange("hcTimeout", value, field.onChange); + const value = parseInt( + e.target.value + ); + handleChange( + "hcTimeout", + value, + field.onChange + ); }} /> @@ -365,14 +530,22 @@ export function HealthCheckFormFields({ name="hcInterval" render={({ field }) => ( - {t("healthyIntervalSeconds")} + + {t("healthyIntervalSeconds")} + { - const value = parseInt(e.target.value); - handleChange("hcInterval", value, field.onChange); + const value = parseInt( + e.target.value + ); + handleChange( + "hcInterval", + value, + field.onChange + ); }} /> @@ -385,13 +558,17 @@ export function HealthCheckFormFields({ name="hcHealthyThreshold" render={({ field }) => ( - {t("healthyThreshold")} + + {t("healthyThreshold")} + { - const value = parseInt(e.target.value); + const value = parseInt( + e.target.value + ); handleChange( "hcHealthyThreshold", value, @@ -413,13 +590,17 @@ export function HealthCheckFormFields({ name="hcUnhealthyInterval" render={({ field }) => ( - {t("unhealthyIntervalSeconds")} + + {t("unhealthyIntervalSeconds")} + { - const value = parseInt(e.target.value); + const value = parseInt( + e.target.value + ); handleChange( "hcUnhealthyInterval", value, @@ -437,13 +618,17 @@ export function HealthCheckFormFields({ name="hcUnhealthyThreshold" render={({ field }) => ( - {t("unhealthyThreshold")} + + {t("unhealthyThreshold")} + { - const value = parseInt(e.target.value); + const value = parseInt( + e.target.value + ); handleChange( "hcUnhealthyThreshold", value, @@ -468,15 +653,24 @@ export function HealthCheckFormFields({ name="hcStatus" render={({ field }) => ( - {t("expectedResponseCodes")} + + {t("expectedResponseCodes")} + { - const val = e.target.value; - const value = val ? parseInt(val) : null; - handleChange("hcStatus", value, field.onChange); + const val = + e.target.value; + const value = val + ? parseInt(val) + : null; + handleChange( + "hcStatus", + value, + field.onChange + ); }} /> @@ -489,7 +683,9 @@ export function HealthCheckFormFields({ name="hcTlsServerName" render={({ field }) => ( - {t("tlsServerName")} + + {t("tlsServerName")} + field.onChange(e) + (v) => + field.onChange( + e + ) ) } /> @@ -539,7 +738,9 @@ export function HealthCheckFormFields({ name="hcHeaders" render={({ field }) => ( - {t("customHeaders")} + + {t("customHeaders")} + handleToggleEnabled(r, v)} /> ); @@ -267,6 +275,7 @@ export default function HealthChecksTable({ { setSelected(r); setDeleteOpen(true); @@ -280,6 +289,7 @@ export default function HealthChecksTable({ + @@ -644,21 +654,25 @@ export default function AlertRuleGraphEditor({ {t("alertingSidebarHint")} - -
- {selectedStep === "source" && ( - - )} - {selectedStep === "trigger" && ( - - )} - {isActionsSidebar && ( -
+ +
+
+ {selectedStep === "source" && ( + + )} + {selectedStep === "trigger" && ( + + )} + {isActionsSidebar && ( +
{t( @@ -719,6 +733,7 @@ export default function AlertRuleGraphEditor({
)}
+