@oneuptime/common 11.5.7 → 11.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/Models/DatabaseModels/AIRun.ts +31 -0
  2. package/Server/API/CodeFixRunAPI.ts +25 -74
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
  6. package/Server/Middleware/ProjectAuthorization.ts +1 -1
  7. package/Server/Services/AIRunService.ts +10 -11
  8. package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
  9. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
  10. package/Types/Email/EmailTemplateType.ts +2 -0
  11. package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
  12. package/build/dist/Models/DatabaseModels/AIRun.js +31 -0
  13. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  14. package/build/dist/Server/API/CodeFixRunAPI.js +25 -53
  15. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
  18. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  20. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
  21. package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
  22. package/build/dist/Server/Services/AIRunService.js +10 -3
  23. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  24. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
  25. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
  26. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  27. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  28. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
  29. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  30. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import Modal, { ModalWidth } from "../Modal/Modal";
2
- import Icon, { IconType, SizeProp } from "../Icon/Icon";
2
+ import Icon, { IconType } from "../Icon/Icon";
3
3
  import IconProp from "../../../Types/Icon/IconProp";
4
4
  import Input from "../Input/Input";
5
5
  import Button, { ButtonStyleType } from "../Button/Button";
@@ -13,6 +13,14 @@ import URL from "../../../Types/API/URL";
13
13
  import { APP_API_URL, BILLING_ENABLED, IS_ENTERPRISE_EDITION, } from "../../Config";
14
14
  import Alert, { AlertType } from "../Alerts/Alert";
15
15
  const ENTERPRISE_URL = "https://oneuptime.com/enterprise/demo";
16
+ const SALES_EMAIL = "sales@oneuptime.com";
17
+ const SALES_MAILTO_URL = "mailto:sales@oneuptime.com";
18
+ /*
19
+ * Seat usage at or above this percent asks the admin to talk to OneUptime about
20
+ * expanding. Matches CRITICAL_CAPACITY_PERCENT in
21
+ * App/FeatureSet/Workers/Jobs/InstanceHealth/EvaluateClickhouseCapacity.ts.
22
+ */
23
+ const SEAT_WARNING_PERCENT = 90;
16
24
  const parseLicenseInstances = (value) => {
17
25
  if (!Array.isArray(value)) {
18
26
  return [];
@@ -197,15 +205,142 @@ const EditionLabel = (props) => {
197
205
  }
198
206
  return currentUserCount > userLimit;
199
207
  }, [licenseValid, userLimit, currentUserCount]);
200
- const userUsagePercent = useMemo(() => {
208
+ /*
209
+ * Unclamped and unrounded. The only value that can tell 120/120 apart from
210
+ * 240/120. Null means there is no seat limit to measure against, either
211
+ * because the license is unlimited or because usage has not been reported.
212
+ */
213
+ const rawUserUsagePercent = useMemo(() => {
201
214
  if (typeof userLimit !== "number" || userLimit <= 0) {
202
215
  return null;
203
216
  }
204
217
  if (typeof currentUserCount !== "number") {
205
218
  return null;
206
219
  }
207
- return Math.min(100, Math.max(0, Math.round((currentUserCount / userLimit) * 100)));
220
+ return Math.max(0, (currentUserCount / userLimit) * 100);
208
221
  }, [userLimit, currentUserCount]);
222
+ /*
223
+ * The percent the admin reads, and the one the warning fires on. May exceed
224
+ * 100 so a breach shows a real figure. Floored to 99 when rounding would
225
+ * claim 100% while a seat is genuinely free, so 599/600 never reads
226
+ * "100% of licensed seats in use" next to "1 seat remaining".
227
+ */
228
+ const seatUsageDisplayPercent = useMemo(() => {
229
+ if (rawUserUsagePercent === null) {
230
+ return null;
231
+ }
232
+ const rounded = Math.round(rawUserUsagePercent);
233
+ if (rounded >= 100 && rawUserUsagePercent < 100) {
234
+ return 99;
235
+ }
236
+ return rounded;
237
+ }, [rawUserUsagePercent]);
238
+ /*
239
+ * The displayed percent clamped to 0-100. Drives the bar width and
240
+ * aria-valuenow, so the fill can never overflow its track and aria-valuenow
241
+ * can never exceed aria-valuemax. Derived from the displayed percent rather
242
+ * than the raw ratio so the fill always agrees with the caption beneath it:
243
+ * 599/600 reads 99% and fills to 99%, not to a visually full 100%. The true
244
+ * figure for a breach goes in aria-valuetext.
245
+ */
246
+ const seatUsageBarPercent = useMemo(() => {
247
+ if (seatUsageDisplayPercent === null) {
248
+ return 0;
249
+ }
250
+ return Math.min(100, Math.max(0, seatUsageDisplayPercent));
251
+ }, [seatUsageDisplayPercent]);
252
+ const seatsRemaining = useMemo(() => {
253
+ if (typeof userLimit !== "number" || userLimit <= 0) {
254
+ return null;
255
+ }
256
+ if (typeof currentUserCount !== "number") {
257
+ return null;
258
+ }
259
+ return userLimit - currentUserCount;
260
+ }, [userLimit, currentUserCount]);
261
+ const seatsRemainingText = useMemo(() => {
262
+ if (typeof seatsRemaining !== "number") {
263
+ return "";
264
+ }
265
+ if (seatsRemaining > 0) {
266
+ return `${seatsRemaining.toLocaleString()} ${seatsRemaining === 1 ? "seat" : "seats"} remaining`;
267
+ }
268
+ if (seatsRemaining === 0) {
269
+ return "No seats remaining";
270
+ }
271
+ return `${Math.abs(seatsRemaining).toLocaleString()} over limit`;
272
+ }, [seatsRemaining]);
273
+ /*
274
+ * Total and mutually exclusive. isUserLimitBreached is tested first, so
275
+ * reaching the >= 90 test structurally guarantees the limit is not exceeded
276
+ * and the amber nudge can never co-fire with the red breach.
277
+ *
278
+ * The test runs on the displayed percent rather than the raw ratio so that
279
+ * the number triggering the warning is the number printed under the bar.
280
+ */
281
+ const seatTone = useMemo(() => {
282
+ if (!licenseValid) {
283
+ return "healthy";
284
+ }
285
+ if (isUserLimitBreached) {
286
+ return "breached";
287
+ }
288
+ if (typeof seatUsageDisplayPercent === "number" &&
289
+ seatUsageDisplayPercent >= SEAT_WARNING_PERCENT) {
290
+ return "approaching";
291
+ }
292
+ return "healthy";
293
+ }, [licenseValid, isUserLimitBreached, seatUsageDisplayPercent]);
294
+ /*
295
+ * Copy containing apostrophes lives in string literals rather than JSX text,
296
+ * because react/no-unescaped-entities is enforced in this repo.
297
+ */
298
+ const seatAdvisoryTitle = useMemo(() => {
299
+ if (seatTone === "breached") {
300
+ const over = Math.abs(seatsRemaining || 0);
301
+ return over === 1
302
+ ? "1 user over your licensed seats"
303
+ : `${over.toLocaleString()} users over your licensed seats`;
304
+ }
305
+ if (seatTone === "approaching") {
306
+ if (seatsRemaining === 0) {
307
+ return "Every licensed seat is in use";
308
+ }
309
+ return seatsRemaining === 1
310
+ ? "Only 1 seat left on your license"
311
+ : `Only ${(seatsRemaining || 0).toLocaleString()} seats left on your license`;
312
+ }
313
+ return "";
314
+ }, [seatTone, seatsRemaining]);
315
+ const seatAdvisoryBody = useMemo(() => {
316
+ const limitText = (userLimit || 0).toLocaleString();
317
+ const inUse = `${(currentUserCount || 0).toLocaleString()} of ${limitText}`;
318
+ if (seatTone === "breached") {
319
+ return `This installation is using ${inUse} licensed seats. Expand your license so it covers everyone on the platform.`;
320
+ }
321
+ if (seatTone === "approaching") {
322
+ return `You're using ${inUse} licensed seats. Anyone you add beyond ${limitText} puts this installation over its licensed limit — if more people are joining, it's worth talking to OneUptime about expanding now.`;
323
+ }
324
+ return "";
325
+ }, [seatTone, currentUserCount, userLimit]);
326
+ const seatUsageAriaValueText = useMemo(() => {
327
+ if (typeof seatUsageDisplayPercent !== "number") {
328
+ return "";
329
+ }
330
+ if (seatTone === "breached") {
331
+ return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatAdvisoryTitle}`;
332
+ }
333
+ if (typeof seatsRemaining === "number") {
334
+ return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatsRemainingText}`;
335
+ }
336
+ return `${seatUsageDisplayPercent}% of licensed seats in use`;
337
+ }, [
338
+ seatUsageDisplayPercent,
339
+ seatTone,
340
+ seatAdvisoryTitle,
341
+ seatsRemaining,
342
+ seatsRemainingText,
343
+ ]);
209
344
  const editionName = useMemo(() => {
210
345
  if (!IS_ENTERPRISE_EDITION) {
211
346
  return "Community Edition";
@@ -227,11 +362,14 @@ const EditionLabel = (props) => {
227
362
  if (!licenseValid) {
228
363
  return "bg-red-500";
229
364
  }
230
- if (isUserLimitBreached) {
365
+ if (seatTone === "breached") {
231
366
  return "bg-red-500";
232
367
  }
368
+ if (seatTone === "approaching") {
369
+ return "bg-amber-500";
370
+ }
233
371
  return "bg-emerald-500";
234
- }, [isConfigLoading, licenseValid, isUserLimitBreached]);
372
+ }, [isConfigLoading, licenseValid, seatTone]);
235
373
  const ctaLabel = useMemo(() => {
236
374
  if (!IS_ENTERPRISE_EDITION) {
237
375
  return "Learn more";
@@ -242,11 +380,84 @@ const EditionLabel = (props) => {
242
380
  if (!licenseValid) {
243
381
  return "Validate license";
244
382
  }
245
- if (isUserLimitBreached) {
383
+ if (seatTone === "breached") {
246
384
  return "User limit exceeded";
247
385
  }
386
+ if (seatTone === "approaching") {
387
+ return "Seats nearly full";
388
+ }
248
389
  return "View details";
390
+ }, [isConfigLoading, licenseValid, seatTone]);
391
+ const pillTone = useMemo(() => {
392
+ if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
393
+ return "normal";
394
+ }
395
+ if (seatTone === "breached" ||
396
+ (!licenseValid && Boolean(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey))) {
397
+ return "alerted";
398
+ }
399
+ if (seatTone === "approaching") {
400
+ return "warning";
401
+ }
402
+ return "normal";
403
+ }, [
404
+ isConfigLoading,
405
+ licenseValid,
406
+ seatTone,
407
+ globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey,
408
+ ]);
409
+ const modalIcon = useMemo(() => {
410
+ if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
411
+ return IconProp.Cube;
412
+ }
413
+ if (!licenseValid || isUserLimitBreached) {
414
+ return IconProp.Alert;
415
+ }
416
+ return IconProp.ShieldCheck;
417
+ }, [isConfigLoading, licenseValid, isUserLimitBreached]);
418
+ const modalIconType = useMemo(() => {
419
+ if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
420
+ return IconType.Info;
421
+ }
422
+ if (!licenseValid || isUserLimitBreached) {
423
+ return IconType.Danger;
424
+ }
425
+ return IconType.Success;
249
426
  }, [isConfigLoading, licenseValid, isUserLimitBreached]);
427
+ const modalDescription = useMemo(() => {
428
+ if (!IS_ENTERPRISE_EDITION) {
429
+ return "You are running the free, open-source build of OneUptime.";
430
+ }
431
+ if (isConfigLoading) {
432
+ return "Checking your license with OneUptime...";
433
+ }
434
+ if (!licenseValid) {
435
+ return "Validate your license key to activate Enterprise Edition.";
436
+ }
437
+ return "License, seat usage, and the instances covered by this key.";
438
+ }, [isConfigLoading, licenseValid]);
439
+ /*
440
+ * The per-instance counts only exceed the unique total when someone actually
441
+ * appears on more than one instance, and the component cannot know whether
442
+ * they do — it receives per-instance counts and the unique total separately.
443
+ * So the summation is stated as a possibility, and is dropped entirely when
444
+ * there is no unique total to compare against. It names the unique user count
445
+ * rather than "the total above", which would be ambiguous next to a hero
446
+ * reading "119 / 120 seats".
447
+ */
448
+ const instanceOverlapText = useMemo(() => {
449
+ const countingRule = `Seats are counted uniquely across all ${licenseInstances.length} instances that share this license — the same person on multiple instances uses one seat.`;
450
+ if (typeof currentUserCount !== "number") {
451
+ return countingRule;
452
+ }
453
+ return `${countingRule} Per-instance counts can therefore add up to more than the ${currentUserCount.toLocaleString()} unique ${currentUserCount === 1 ? "user" : "users"} counted above.`;
454
+ }, [licenseInstances.length, currentUserCount]);
455
+ const licenseKeyHelperText = useMemo(() => {
456
+ if (isChangingLicense) {
457
+ return "Enter the new enterprise license key and validate it to replace the current one. Your existing license stays active until the new key is validated.";
458
+ }
459
+ return "You have installed Enterprise Edition of OneUptime. You need to validate your license key. Need a license key? Contact our sales team at";
460
+ }, [isChangingLicense]);
250
461
  const communityFeatures = useMemo(() => {
251
462
  return [
252
463
  "Full OneUptime platform with incident response, status pages, and workflow automation.",
@@ -299,6 +510,26 @@ const EditionLabel = (props) => {
299
510
  }
300
511
  closeDialog();
301
512
  };
513
+ const buildSeatExpansionMailto = () => {
514
+ const subject = seatTone === "breached"
515
+ ? "Expand OneUptime license - over seat limit"
516
+ : "Expand OneUptime license - nearly out of seats";
517
+ const body = [
518
+ "Hi OneUptime,",
519
+ "",
520
+ "We would like to expand the seat count on our enterprise license.",
521
+ "",
522
+ `Company: ${(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseCompanyName) || "Not specified"}`,
523
+ `Seats in use: ${(currentUserCount || 0).toLocaleString()} of ${(userLimit || 0).toLocaleString()}`,
524
+ `Instances on this license: ${licenseInstances.length}`,
525
+ ].join("\n");
526
+ return `${SALES_MAILTO_URL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
527
+ };
528
+ const handleRequestMoreSeats = () => {
529
+ if (typeof window !== "undefined") {
530
+ window.location.href = buildSeatExpansionMailto();
531
+ }
532
+ };
302
533
  const runLicenseValidation = useCallback(async (key, setLoading) => {
303
534
  const trimmedKey = key.trim();
304
535
  if (!trimmedKey) {
@@ -348,6 +579,20 @@ const EditionLabel = (props) => {
348
579
  };
349
580
  const showLicenseKeyInput = IS_ENTERPRISE_EDITION && (!licenseValid || isChangingLicense);
350
581
  const shouldShowEnterpriseValidationButton = showLicenseKeyInput;
582
+ /*
583
+ * Collapses the !configError && !isConfigLoading && licenseValid chain that
584
+ * gates the stat strip, the seat card and the instances card.
585
+ */
586
+ const showLicenseDetails = IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && licenseValid;
587
+ const showEnterpriseFeatureList = IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && !licenseValid;
588
+ /*
589
+ * GlobalConfigAPI returns userLimit and currentUserCount to anonymous callers
590
+ * because the same route serves the signed-out login page, and gates only
591
+ * licenseKey, token, instances and instanceId on isAuthenticatedUser. Gate
592
+ * the sales ask on a field the server does redact, so a signed-out visitor is
593
+ * never shown an administrator-targeted CTA.
594
+ */
595
+ const canSeeLicenseAdmin = Boolean(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey);
351
596
  const modalSubmitButtonText = IS_ENTERPRISE_EDITION
352
597
  ? shouldShowEnterpriseValidationButton
353
598
  ? "Validate License"
@@ -366,169 +611,209 @@ const EditionLabel = (props) => {
366
611
  ? !licenseKeyInput.trim() || isValidating || isConfigLoading
367
612
  : undefined
368
613
  : false;
369
- const showAlertedPill = IS_ENTERPRISE_EDITION &&
370
- !isConfigLoading &&
371
- (isUserLimitBreached ||
372
- (!licenseValid && Boolean(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey)));
373
- const pillClassName = showAlertedPill
614
+ const modalRightElement = showLicenseDetails ? (React.createElement("span", { className: `inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${seatTone === "breached"
615
+ ? "border-red-200 bg-red-50 text-red-700"
616
+ : seatTone === "approaching"
617
+ ? "border-amber-200 bg-amber-50 text-amber-800"
618
+ : "border-emerald-200 bg-emerald-50 text-emerald-800"}` },
619
+ React.createElement("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${seatTone === "breached"
620
+ ? "bg-red-500"
621
+ : seatTone === "approaching"
622
+ ? "bg-amber-500"
623
+ : "bg-emerald-500"}` }),
624
+ seatTone === "breached"
625
+ ? "Seat limit exceeded"
626
+ : seatTone === "approaching"
627
+ ? "Seats nearly full"
628
+ : "License active")) : undefined;
629
+ const modalLeftFooterElement = showLicenseDetails && !isChangingLicense ? (React.createElement(Button, { title: "Change license key", icon: IconProp.Edit, buttonStyle: ButtonStyleType.NORMAL, onClick: handleStartChangingLicense })) : undefined;
630
+ const pillClassName = pillTone === "alerted"
374
631
  ? "group inline-flex items-center gap-2 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-700 shadow-sm transition hover:border-red-300 hover:bg-red-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
375
- : "group inline-flex items-center gap-2 rounded-full border border-indigo-100 bg-white px-3 py-1 text-xs font-medium text-indigo-700 shadow-sm transition hover:border-indigo-300 hover:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
376
- const pillCtaTextClassName = showAlertedPill
632
+ : pillTone === "warning"
633
+ ? "group inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-medium text-amber-800 shadow-sm transition hover:border-amber-300 hover:bg-amber-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400"
634
+ : "group inline-flex items-center gap-2 rounded-full border border-indigo-100 bg-white px-3 py-1 text-xs font-medium text-indigo-700 shadow-sm transition hover:border-indigo-300 hover:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
635
+ const pillCtaTextClassName = pillTone === "alerted"
377
636
  ? "text-[11px] text-red-500 group-hover:text-red-600"
378
- : "text-[11px] text-indigo-500 group-hover:text-indigo-600";
637
+ : pillTone === "warning"
638
+ ? "text-[11px] text-amber-600 group-hover:text-amber-700"
639
+ : "text-[11px] text-indigo-500 group-hover:text-indigo-600";
379
640
  return (React.createElement(React.Fragment, null,
380
641
  React.createElement("button", { type: "button", onClick: openDialog, className: `${pillClassName} ${props.className ? props.className : ""}`, "aria-label": `${editionName}, ${ctaLabel}` },
381
- showAlertedPill && (React.createElement(Icon, { icon: IconProp.Alert, size: SizeProp.Small, className: "text-red-600" })),
642
+ pillTone !== "normal" && (React.createElement(Icon, { icon: pillTone === "alerted"
643
+ ? IconProp.Alert
644
+ : IconProp.ExclaimationCircle, className: `h-3 w-3 ${pillTone === "alerted" ? "text-red-600" : "text-amber-600"}` })),
382
645
  React.createElement("span", { className: `h-2 w-2 rounded-full transition group-hover:scale-110 ${indicatorColor}` }),
383
646
  React.createElement("span", { className: "tracking-wide" }, editionName),
384
647
  React.createElement("span", { className: pillCtaTextClassName }, ctaLabel)),
385
- isDialogOpen && (React.createElement(Modal, { title: editionName, submitButtonText: modalSubmitButtonText, closeButtonText: "Close", onClose: closeDialog, onSubmit: modalOnSubmit, modalWidth: ModalWidth.Large, isLoading: modalIsLoading, disableSubmitButton: modalDisableSubmitButton, isBodyLoading: IS_ENTERPRISE_EDITION ? isConfigLoading : false },
386
- React.createElement("div", { className: "space-y-3 text-sm text-gray-600" }, IS_ENTERPRISE_EDITION ? (React.createElement(React.Fragment, null,
387
- configError && (React.createElement("div", { className: "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700" },
388
- React.createElement("p", { className: "font-semibold" }, "Unable to load license details"),
389
- React.createElement("p", { className: "mt-1" }, configError),
390
- React.createElement("div", { className: "mt-3 -ml-3" },
391
- React.createElement(Button, { title: "Retry", buttonStyle: ButtonStyleType.DANGER, onClick: handleRetryFetch, isLoading: isConfigLoading })))),
392
- !configError && !isConfigLoading && licenseValid && (React.createElement("div", { className: "rounded-md border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700" },
393
- React.createElement("p", { className: "font-semibold" }, "License verified"),
394
- React.createElement("p", { className: "mt-1" },
395
- React.createElement("span", { className: "font-medium" }, "Company:"),
396
- " ",
397
- (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseCompanyName) || "Not specified"),
398
- licenseExpiresAtText && (React.createElement("p", null,
399
- React.createElement("span", { className: "font-medium" }, "Expires:"),
400
- " ",
401
- licenseExpiresAtText)))),
402
- !configError && !isConfigLoading && licenseValid && (React.createElement("div", { className: `rounded-lg border p-4 shadow-sm ${isUserLimitBreached
403
- ? "border-red-200 bg-red-50"
404
- : "border-gray-200 bg-white"}` },
405
- React.createElement("div", { className: "flex items-start gap-3" },
406
- React.createElement("div", { className: `flex h-9 w-9 items-center justify-center rounded-full ${isUserLimitBreached
407
- ? "bg-red-100 text-red-600"
408
- : "bg-indigo-100 text-indigo-600"}` },
409
- React.createElement(Icon, { icon: isUserLimitBreached ? IconProp.Alert : IconProp.User, size: SizeProp.Regular })),
410
- React.createElement("div", { className: "flex-1" },
411
- React.createElement("div", { className: "flex items-center justify-between" },
412
- React.createElement("h3", { className: `text-sm font-semibold ${isUserLimitBreached
413
- ? "text-red-900"
414
- : "text-gray-900"}` }, "User Usage"),
415
- isUserLimitBreached && (React.createElement("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700" }, "Limit exceeded"))),
416
- React.createElement("div", { className: "mt-2 flex items-baseline gap-1" },
417
- React.createElement("span", { className: `text-2xl font-semibold ${isUserLimitBreached
648
+ isDialogOpen && (React.createElement(Modal, { title: editionName, description: modalDescription, icon: modalIcon, iconType: modalIconType, rightElement: modalRightElement, submitButtonText: modalSubmitButtonText, closeButtonText: "Close", onClose: closeDialog, onSubmit: modalOnSubmit, modalWidth: ModalWidth.Medium, isLoading: modalIsLoading, disableSubmitButton: modalDisableSubmitButton, isBodyLoading: IS_ENTERPRISE_EDITION ? isConfigLoading : false, leftFooterElement: modalLeftFooterElement },
649
+ React.createElement("div", { className: "space-y-4 text-sm text-gray-600" }, IS_ENTERPRISE_EDITION ? (React.createElement(React.Fragment, null,
650
+ !configError && successMessage && (React.createElement(Alert, { type: AlertType.SUCCESS, title: successMessage })),
651
+ configError && (React.createElement("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4" },
652
+ React.createElement("div", { className: "flex items-start gap-2.5" },
653
+ React.createElement(Icon, { icon: IconProp.Alert, className: "mt-0.5 h-4 w-4 shrink-0 text-red-600" }),
654
+ React.createElement("div", { className: "min-w-0 flex-1" },
655
+ React.createElement("p", { className: "text-sm font-semibold text-red-900" }, "Unable to load license details"),
656
+ React.createElement("p", { className: "mt-1 text-xs leading-relaxed text-red-800" }, configError),
657
+ React.createElement("div", { className: "mt-3" },
658
+ React.createElement(Button, { title: "Try again", buttonStyle: ButtonStyleType.DANGER, onClick: handleRetryFetch, isLoading: isConfigLoading, className: "!mt-0 md:!ml-0" })))))),
659
+ showLicenseDetails && (React.createElement("dl", { className: "grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2" },
660
+ React.createElement("div", { className: "min-w-0 bg-white px-4 py-3" },
661
+ React.createElement("dt", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-500" }, "Licensed to"),
662
+ React.createElement("dd", { className: "mt-1 truncate text-sm font-medium text-gray-900", title: (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseCompanyName) || undefined }, (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseCompanyName) || "Not specified")),
663
+ React.createElement("div", { className: "bg-white px-4 py-3" },
664
+ React.createElement("dt", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-500" }, "Expires"),
665
+ React.createElement("dd", { className: "mt-1 text-sm font-medium tabular-nums text-gray-900" }, licenseExpiresAtText || "—")))),
666
+ showLicenseDetails && (React.createElement("section", { "aria-labelledby": "edition-seats-heading", className: "rounded-xl border border-gray-200 bg-white p-5" },
667
+ React.createElement("div", { className: "flex items-start justify-between gap-4" },
668
+ React.createElement("div", { className: "min-w-0" },
669
+ React.createElement("h4", { id: "edition-seats-heading", className: "text-sm font-semibold text-gray-900" }, "Licensed seats"),
670
+ React.createElement("p", { className: "mt-0.5 text-xs text-gray-500" }, "Unique users across every instance on this license.")),
671
+ seatTone !== "healthy" && (React.createElement("span", { className: `inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${seatTone === "breached"
672
+ ? "bg-red-100 text-red-700"
673
+ : "bg-amber-100 text-amber-800"}` }, seatTone === "breached"
674
+ ? "Limit exceeded"
675
+ : "Nearly full"))),
676
+ React.createElement("div", { className: "mt-4 flex items-end justify-between gap-4" },
677
+ React.createElement("p", { className: "flex items-baseline gap-1.5" },
678
+ React.createElement("span", { className: `text-3xl font-semibold leading-none tabular-nums ${typeof currentUserCount !== "number"
679
+ ? "text-gray-300"
680
+ : seatTone === "breached"
418
681
  ? "text-red-700"
419
682
  : "text-gray-900"}` }, typeof currentUserCount === "number"
420
- ? currentUserCount.toLocaleString()
421
- : "—"),
422
- React.createElement("span", { className: "text-sm text-gray-500" },
423
- " / ",
424
- typeof userLimit === "number" && userLimit > 0
425
- ? `${userLimit.toLocaleString()} users`
426
- : "unlimited")),
427
- typeof userUsagePercent === "number" && (React.createElement("div", { className: "mt-3" },
428
- React.createElement("div", { className: "h-2 w-full overflow-hidden rounded-full bg-gray-200" },
429
- React.createElement("div", { className: `h-full rounded-full transition-all ${isUserLimitBreached
430
- ? "bg-red-500"
431
- : userUsagePercent >= 80
432
- ? "bg-amber-500"
433
- : "bg-emerald-500"}`, style: { width: `${userUsagePercent}%` } })),
434
- React.createElement("p", { className: "mt-1 text-xs text-gray-500" },
435
- userUsagePercent,
436
- "% of licensed seats in use"))),
437
- isUserLimitBreached && (React.createElement("p", { className: "mt-3 text-xs text-red-700" },
438
- "Your installation has more users than your license permits. Please contact",
439
- " ",
440
- React.createElement("a", { href: "mailto:sales@oneuptime.com", className: "font-medium text-red-800 underline hover:text-red-900" }, "sales@oneuptime.com"),
441
- " ",
442
- "to expand your license.")),
443
- licenseInstances.length > 1 && (React.createElement("p", { className: "mt-3 text-xs text-gray-500" },
444
- "Users are counted uniquely across all",
445
- " ",
446
- licenseInstances.length,
447
- " instances that share this license \u2014 the same user on multiple instances uses one seat.")),
448
- React.createElement("p", { className: "mt-3 text-xs text-gray-500" }, userCountUpdatedAtText
449
- ? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
450
- : "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours."))))),
451
- !configError &&
452
- !isConfigLoading &&
453
- licenseValid &&
454
- licenseInstances.length > 0 && (React.createElement("div", { className: "rounded-lg border border-gray-200 bg-white p-4 shadow-sm" },
455
- React.createElement("div", { className: "flex items-center justify-between" },
456
- React.createElement("h3", { className: "text-sm font-semibold text-gray-900" }, "Instances using this license"),
457
- React.createElement("span", { className: "text-xs text-gray-500" },
683
+ ? currentUserCount.toLocaleString()
684
+ : "—"),
685
+ React.createElement("span", { className: "text-sm tabular-nums text-gray-500" },
686
+ " / ",
687
+ typeof userLimit === "number" && userLimit > 0
688
+ ? `${userLimit.toLocaleString()} seats`
689
+ : "unlimited")),
690
+ typeof seatsRemaining === "number" && (React.createElement("p", { className: `text-xs font-medium tabular-nums ${seatTone === "breached"
691
+ ? "text-red-700"
692
+ : seatTone === "approaching"
693
+ ? "text-amber-700"
694
+ : "text-gray-500"}` }, seatsRemainingText))),
695
+ typeof seatUsageDisplayPercent === "number" && (React.createElement("div", { className: "mt-2.5" },
696
+ React.createElement("div", { className: "h-2.5 w-full overflow-hidden rounded-full bg-gray-100", role: "progressbar", "aria-label": "Licensed seat usage", "aria-valuenow": seatUsageBarPercent, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuetext": seatUsageAriaValueText },
697
+ React.createElement("div", { className: `h-full rounded-full transition-all duration-300 ease-out ${seatTone === "breached"
698
+ ? "bg-red-500"
699
+ : seatTone === "approaching"
700
+ ? "bg-amber-500"
701
+ : "bg-emerald-500"}`, style: { width: `${seatUsageBarPercent}%` } })),
702
+ React.createElement("p", { className: "mt-1.5 text-xs tabular-nums text-gray-500" },
703
+ seatUsageDisplayPercent,
704
+ "% of licensed seats in use"))),
705
+ seatTone !== "healthy" && canSeeLicenseAdmin && (React.createElement("div", { role: seatTone === "breached" ? "alert" : "status", className: `mt-4 rounded-lg border p-3.5 ${seatTone === "breached"
706
+ ? "border-red-200 bg-red-50"
707
+ : "border-amber-200 bg-amber-50"}` },
708
+ React.createElement("div", { className: "flex items-start gap-2.5" },
709
+ React.createElement(Icon, { icon: seatTone === "breached"
710
+ ? IconProp.Alert
711
+ : IconProp.ExclaimationCircle, className: `mt-0.5 h-4 w-4 shrink-0 ${seatTone === "breached"
712
+ ? "text-red-600"
713
+ : "text-amber-600"}` }),
714
+ React.createElement("div", { className: "min-w-0 flex-1" },
715
+ React.createElement("h5", { className: `text-sm font-semibold ${seatTone === "breached"
716
+ ? "text-red-900"
717
+ : "text-amber-900"}` }, seatAdvisoryTitle),
718
+ React.createElement("p", { className: `mt-1 text-xs leading-relaxed ${seatTone === "breached"
719
+ ? "text-red-800"
720
+ : "text-amber-800"}` }, seatAdvisoryBody),
721
+ React.createElement("div", { className: "mt-3 flex flex-wrap items-center gap-x-3 gap-y-2" },
722
+ React.createElement(Button, { title: seatTone === "breached"
723
+ ? "Expand your license"
724
+ : "Request more seats", icon: IconProp.Email, buttonStyle: seatTone === "breached"
725
+ ? ButtonStyleType.DANGER
726
+ : ButtonStyleType.PRIMARY, onClick: handleRequestMoreSeats, className: "!mt-0 md:!ml-0" }),
727
+ React.createElement("span", { className: `text-[11px] ${seatTone === "breached"
728
+ ? "text-red-700"
729
+ : "text-amber-700"}` },
730
+ "Or email",
731
+ " ",
732
+ React.createElement("a", { href: SALES_MAILTO_URL, className: `font-medium underline ${seatTone === "breached"
733
+ ? "text-red-800 hover:text-red-900"
734
+ : "text-amber-900 hover:text-amber-950"}` }, SALES_EMAIL),
735
+ " ",
736
+ "directly.")))))),
737
+ React.createElement("div", { className: "mt-4 border-t border-gray-100 pt-3" },
738
+ React.createElement("p", { className: "text-xs text-gray-500" }, userCountUpdatedAtText
739
+ ? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
740
+ : "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours.")))),
741
+ showLicenseDetails && licenseInstances.length > 0 && (React.createElement("section", { "aria-labelledby": "edition-instances-heading", className: "rounded-xl border border-gray-200 bg-white p-5" },
742
+ React.createElement("div", { className: "flex items-center justify-between gap-3" },
743
+ React.createElement("h4", { id: "edition-instances-heading", className: "text-sm font-semibold text-gray-900" }, "Instances on this license"),
744
+ React.createElement("span", { className: "shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium tabular-nums text-gray-600" },
458
745
  licenseInstances.length,
459
746
  " ",
460
747
  licenseInstances.length === 1
461
748
  ? "instance"
462
749
  : "instances")),
463
- React.createElement("p", { className: "mt-1 text-xs text-gray-500" }, "Use the same license key on every instance you deploy (staging, production, and so on). Each instance reports its usage daily."),
464
- React.createElement("ul", { className: "mt-3 divide-y divide-gray-100" }, licenseInstances.map((instance, index) => {
750
+ React.createElement("p", { className: "mt-1 text-xs leading-relaxed text-gray-500" }, "Use the same license key on every instance you deploy (staging, production, and so on). Each instance reports its usage daily."),
751
+ licenseInstances.length > 1 && (React.createElement("p", { className: "mt-1.5 text-xs leading-relaxed text-gray-500" }, instanceOverlapText)),
752
+ React.createElement("ul", { className: "mt-3 divide-y divide-gray-100 overflow-hidden rounded-lg border border-gray-200" }, licenseInstances.map((instance, index) => {
465
753
  const isThisInstance = Boolean(thisInstanceId) &&
466
754
  instance.instanceId === thisInstanceId;
467
- return (React.createElement("li", { key: instance.instanceId || index, className: "flex items-center justify-between gap-3 py-2" },
468
- React.createElement("div", { className: "min-w-0" },
469
- React.createElement("p", { className: "truncate text-sm font-medium text-gray-900" },
470
- instance.host || "Unknown host",
471
- isThisInstance && (React.createElement("span", { className: "ml-2 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-medium text-indigo-700" }, "This instance"))),
472
- React.createElement("p", { className: "text-xs text-gray-500" }, formatInstanceReportedAt(instance.lastReportedAt))),
473
- React.createElement("div", { className: "shrink-0 text-right text-sm text-gray-700" }, typeof instance.userCount === "number"
474
- ? `${instance.userCount.toLocaleString()} ${instance.userCount === 1
755
+ return (React.createElement("li", { key: instance.instanceId || index, className: "flex items-center justify-between gap-4 bg-white px-3 py-2.5 transition-colors hover:bg-gray-50" },
756
+ React.createElement("div", { className: "min-w-0 flex-1" },
757
+ React.createElement("div", { className: "flex min-w-0 items-center gap-2" },
758
+ React.createElement("span", { className: "truncate text-sm font-medium text-gray-900", title: instance.host || "Unknown host" }, instance.host || "Unknown host"),
759
+ isThisInstance && (React.createElement("span", { className: "shrink-0 rounded-full bg-indigo-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700" }, "This instance"))),
760
+ React.createElement("p", { className: "mt-0.5 truncate text-xs text-gray-500" }, formatInstanceReportedAt(instance.lastReportedAt))),
761
+ React.createElement("div", { className: "flex shrink-0 items-baseline justify-end gap-1" }, typeof instance.userCount === "number" ? (React.createElement(React.Fragment, null,
762
+ React.createElement("span", { className: "text-sm font-medium tabular-nums text-gray-900" }, instance.userCount.toLocaleString()),
763
+ React.createElement("span", { className: "text-xs text-gray-500" }, instance.userCount === 1
475
764
  ? "user"
476
- : "users"}`
477
- : "—")));
765
+ : "users"))) : (React.createElement("span", { className: "text-sm text-gray-400" }, "\u2014")))));
478
766
  })))),
479
- !configError &&
480
- !isConfigLoading &&
481
- licenseValid &&
482
- !isChangingLicense && (React.createElement("div", { className: "-ml-3" },
483
- React.createElement(Button, { title: "Change license key", icon: IconProp.Edit, buttonStyle: ButtonStyleType.NORMAL, onClick: handleStartChangingLicense }))),
484
767
  !configError &&
485
768
  !isConfigLoading &&
486
769
  !licenseValid &&
487
- (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey) && (React.createElement("div", { className: "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700" },
488
- React.createElement("p", { className: "font-semibold" }, "License validation required"),
489
- React.createElement("p", { className: "mt-1" }, "The stored license information could not be verified. Please validate the license key again."))),
490
- !configError && (React.createElement(React.Fragment, null,
491
- successMessage && (React.createElement(Alert, { type: AlertType.SUCCESS, title: successMessage })),
492
- showLicenseKeyInput && (React.createElement(React.Fragment, null,
493
- React.createElement("div", null,
494
- React.createElement("label", { className: "text-sm font-medium text-gray-700" }, isChangingLicense
495
- ? "New License Key"
496
- : "License Key"),
497
- React.createElement(Input, { value: licenseKeyInput, onChange: (value) => {
498
- setLicenseKeyInput(value);
499
- licenseInputEditedRef.current = true;
500
- }, placeholder: "Enter your enterprise license key", disableSpellCheck: true })),
501
- validationError && (React.createElement(Alert, { type: AlertType.DANGER, title: validationError })),
502
- isChangingLicense ? (React.createElement("div", { className: "flex items-center justify-between gap-3" },
503
- React.createElement("p", { className: "text-xs text-gray-500" }, "Enter the new enterprise license key and validate it to replace the current one. Your existing license stays active until the new key is validated."),
504
- React.createElement(Button, { title: "Cancel", buttonStyle: ButtonStyleType.NORMAL, onClick: handleCancelChangingLicense }))) : (React.createElement("p", { className: "text-xs text-gray-500" },
505
- "You have installed Enterprise Edition of OneUptime. You need to validate your license key. Need a license key? Contact our sales team at",
506
- " ",
507
- React.createElement("a", { href: "mailto:sales@oneuptime.com", className: "font-medium text-indigo-600 hover:text-indigo-700" }, "sales@oneuptime.com"),
508
- ".")))))),
509
- React.createElement("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm" },
510
- React.createElement("h3", { className: "text-sm font-semibold text-indigo-900" }, "Enterprise Edition Features"),
511
- React.createElement("ul", { className: "mt-3 space-y-2 text-sm text-indigo-900" }, enterpriseFeatures.map((feature, index) => {
512
- return (React.createElement("li", { key: index, className: "flex items-start gap-2" },
513
- React.createElement(Icon, { icon: IconProp.Check, type: IconType.Success, size: SizeProp.Small, className: "mt-0.5" }),
514
- React.createElement("span", { className: "leading-snug" }, feature)));
515
- })),
516
- React.createElement("p", { className: "mt-3 text-xs text-indigo-700" }, "Already have a license? Validate it above to unlock these premium capabilities immediately.")))) : (React.createElement(React.Fragment, null,
770
+ (globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.enterpriseLicenseKey) && (React.createElement("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4" },
771
+ React.createElement("p", { className: "text-sm font-semibold text-red-900" }, "License validation required"),
772
+ React.createElement("p", { className: "mt-1 text-xs leading-relaxed text-red-800" }, "The stored license information could not be verified. Please validate the license key again."))),
773
+ !configError && showLicenseKeyInput && (React.createElement("section", { className: "rounded-xl border border-gray-200 bg-white p-5" },
774
+ React.createElement("div", { className: "flex items-start justify-between gap-4" },
775
+ React.createElement("div", { className: "min-w-0" },
776
+ React.createElement("label", { htmlFor: "enterprise-license-key", className: "text-sm font-semibold text-gray-900" }, isChangingLicense
777
+ ? "New license key"
778
+ : "License key"),
779
+ React.createElement("p", { className: "mt-0.5 text-xs leading-relaxed text-gray-500" },
780
+ licenseKeyHelperText,
781
+ !isChangingLicense && (React.createElement(React.Fragment, null,
782
+ " ",
783
+ React.createElement("a", { href: SALES_MAILTO_URL, className: "font-medium text-indigo-600 hover:text-indigo-700" }, SALES_EMAIL),
784
+ ".")))),
785
+ isChangingLicense && (React.createElement(Button, { title: "Cancel", buttonStyle: ButtonStyleType.NORMAL, onClick: handleCancelChangingLicense, className: "!mt-0 shrink-0 md:!ml-0" }))),
786
+ React.createElement("div", { className: "mt-3" },
787
+ React.createElement(Input, { id: "enterprise-license-key", value: licenseKeyInput, onChange: (value) => {
788
+ setLicenseKeyInput(value);
789
+ licenseInputEditedRef.current = true;
790
+ }, placeholder: "Enter your enterprise license key", disableSpellCheck: true })),
791
+ validationError && (React.createElement("div", { className: "mt-3" },
792
+ React.createElement(Alert, { type: AlertType.DANGER, title: validationError }))))),
793
+ showEnterpriseFeatureList && (React.createElement("section", { "aria-labelledby": "edition-features-heading", className: "rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-5" },
794
+ React.createElement("h4", { id: "edition-features-heading", className: "text-sm font-semibold text-indigo-900" }, "What your license unlocks"),
795
+ React.createElement("p", { className: "mt-0.5 text-xs text-indigo-700" }, "Validate your key above to turn these on immediately."),
796
+ React.createElement("ul", { className: "mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2" }, enterpriseFeatures.map((feature, index) => {
797
+ return (React.createElement("li", { key: index, className: "flex items-start gap-2 rounded-lg border border-gray-100 bg-white px-3 py-2.5" },
798
+ React.createElement("span", { className: "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-indigo-50" },
799
+ React.createElement(Icon, { icon: IconProp.Check, className: "h-3 w-3 text-indigo-600" })),
800
+ React.createElement("span", { className: "text-xs leading-snug text-gray-700" }, feature)));
801
+ })))))) : (React.createElement(React.Fragment, null,
517
802
  React.createElement("p", null, "You are running the Community Edition of OneUptime. Here is a quick comparison to help you decide if Enterprise is the right fit for your team."),
518
803
  React.createElement("div", { className: "grid gap-4 md:grid-cols-2" },
519
- React.createElement("div", { className: "rounded-lg border border-gray-200 bg-white p-4 shadow-sm" },
520
- React.createElement("h3", { className: "text-sm font-semibold text-gray-900" }, "Community Edition"),
804
+ React.createElement("div", { className: "rounded-xl border border-gray-200 bg-white p-4" },
805
+ React.createElement("h4", { className: "text-sm font-semibold text-gray-900" }, "Community Edition"),
521
806
  React.createElement("ul", { className: "mt-3 space-y-2 text-sm text-gray-600" }, communityFeatures.map((feature, index) => {
522
807
  return (React.createElement("li", { key: index, className: "flex items-start gap-2" },
523
- React.createElement(Icon, { icon: IconProp.Check, size: SizeProp.Small, className: "mt-0.5 text-gray-400" }),
808
+ React.createElement(Icon, { icon: IconProp.Check, className: "mt-0.5 h-3 w-3 shrink-0 text-gray-400" }),
524
809
  React.createElement("span", { className: "leading-snug" }, feature)));
525
810
  })),
526
811
  React.createElement("p", { className: "mt-3 text-xs text-gray-500" }, "Best for small teams experimenting with reliability workflows.")),
527
- React.createElement("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm" },
528
- React.createElement("h3", { className: "text-sm font-semibold text-indigo-900" }, "Enterprise Edition"),
812
+ React.createElement("div", { className: "rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-4" },
813
+ React.createElement("h4", { className: "text-sm font-semibold text-indigo-900" }, "Enterprise Edition"),
529
814
  React.createElement("ul", { className: "mt-3 space-y-2 text-sm text-indigo-900" }, enterpriseFeatures.map((feature, index) => {
530
815
  return (React.createElement("li", { key: index, className: "flex items-start gap-2" },
531
- React.createElement(Icon, { icon: IconProp.Check, type: IconType.Success, size: SizeProp.Small, className: "mt-0.5" }),
816
+ React.createElement(Icon, { icon: IconProp.Check, className: "mt-0.5 h-3 w-3 shrink-0 text-indigo-600" }),
532
817
  React.createElement("span", { className: "leading-snug" }, feature)));
533
818
  })),
534
819
  React.createElement("p", { className: "mt-3 text-xs text-indigo-700" }, "Everything in Community plus white-glove onboarding, enterprise SLAs, and a partner dedicated to your reliability goals."))),