@oneuptime/common 11.5.6 → 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.
- package/Models/DatabaseModels/AIRun.ts +31 -0
- package/Server/API/CodeFixRunAPI.ts +25 -74
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.ts +11 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
- package/Server/Middleware/ProjectAuthorization.ts +1 -1
- package/Server/Services/AIRunService.ts +10 -11
- package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
- package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
- package/build/dist/Models/DatabaseModels/AIRun.js +31 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Server/API/CodeFixRunAPI.js +25 -53
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.js +9 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
- package/build/dist/Server/Services/AIRunService.js +10 -3
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
2
|
-
import Icon, { IconType
|
|
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";
|
|
@@ -34,6 +34,19 @@ export interface ComponentProps {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const ENTERPRISE_URL: string = "https://oneuptime.com/enterprise/demo";
|
|
37
|
+
const SALES_EMAIL: string = "sales@oneuptime.com";
|
|
38
|
+
const SALES_MAILTO_URL: string = "mailto:sales@oneuptime.com";
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Seat usage at or above this percent asks the admin to talk to OneUptime about
|
|
42
|
+
* expanding. Matches CRITICAL_CAPACITY_PERCENT in
|
|
43
|
+
* App/FeatureSet/Workers/Jobs/InstanceHealth/EvaluateClickhouseCapacity.ts.
|
|
44
|
+
*/
|
|
45
|
+
const SEAT_WARNING_PERCENT: number = 90;
|
|
46
|
+
|
|
47
|
+
type SeatTone = "healthy" | "approaching" | "breached";
|
|
48
|
+
|
|
49
|
+
type PillTone = "normal" | "warning" | "alerted";
|
|
37
50
|
|
|
38
51
|
type ParseLicenseInstancesFunction = (
|
|
39
52
|
value: unknown,
|
|
@@ -310,7 +323,12 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
310
323
|
return currentUserCount > userLimit;
|
|
311
324
|
}, [licenseValid, userLimit, currentUserCount]);
|
|
312
325
|
|
|
313
|
-
|
|
326
|
+
/*
|
|
327
|
+
* Unclamped and unrounded. The only value that can tell 120/120 apart from
|
|
328
|
+
* 240/120. Null means there is no seat limit to measure against, either
|
|
329
|
+
* because the license is unlimited or because usage has not been reported.
|
|
330
|
+
*/
|
|
331
|
+
const rawUserUsagePercent: number | null = useMemo(() => {
|
|
314
332
|
if (typeof userLimit !== "number" || userLimit <= 0) {
|
|
315
333
|
return null;
|
|
316
334
|
}
|
|
@@ -319,12 +337,165 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
319
337
|
return null;
|
|
320
338
|
}
|
|
321
339
|
|
|
322
|
-
return Math.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
return Math.max(0, (currentUserCount / userLimit) * 100);
|
|
341
|
+
}, [userLimit, currentUserCount]);
|
|
342
|
+
|
|
343
|
+
/*
|
|
344
|
+
* The percent the admin reads, and the one the warning fires on. May exceed
|
|
345
|
+
* 100 so a breach shows a real figure. Floored to 99 when rounding would
|
|
346
|
+
* claim 100% while a seat is genuinely free, so 599/600 never reads
|
|
347
|
+
* "100% of licensed seats in use" next to "1 seat remaining".
|
|
348
|
+
*/
|
|
349
|
+
const seatUsageDisplayPercent: number | null = useMemo(() => {
|
|
350
|
+
if (rawUserUsagePercent === null) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const rounded: number = Math.round(rawUserUsagePercent);
|
|
355
|
+
|
|
356
|
+
if (rounded >= 100 && rawUserUsagePercent < 100) {
|
|
357
|
+
return 99;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return rounded;
|
|
361
|
+
}, [rawUserUsagePercent]);
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* The displayed percent clamped to 0-100. Drives the bar width and
|
|
365
|
+
* aria-valuenow, so the fill can never overflow its track and aria-valuenow
|
|
366
|
+
* can never exceed aria-valuemax. Derived from the displayed percent rather
|
|
367
|
+
* than the raw ratio so the fill always agrees with the caption beneath it:
|
|
368
|
+
* 599/600 reads 99% and fills to 99%, not to a visually full 100%. The true
|
|
369
|
+
* figure for a breach goes in aria-valuetext.
|
|
370
|
+
*/
|
|
371
|
+
const seatUsageBarPercent: number = useMemo(() => {
|
|
372
|
+
if (seatUsageDisplayPercent === null) {
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return Math.min(100, Math.max(0, seatUsageDisplayPercent));
|
|
377
|
+
}, [seatUsageDisplayPercent]);
|
|
378
|
+
|
|
379
|
+
const seatsRemaining: number | null = useMemo(() => {
|
|
380
|
+
if (typeof userLimit !== "number" || userLimit <= 0) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (typeof currentUserCount !== "number") {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return userLimit - currentUserCount;
|
|
326
389
|
}, [userLimit, currentUserCount]);
|
|
327
390
|
|
|
391
|
+
const seatsRemainingText: string = useMemo(() => {
|
|
392
|
+
if (typeof seatsRemaining !== "number") {
|
|
393
|
+
return "";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (seatsRemaining > 0) {
|
|
397
|
+
return `${seatsRemaining.toLocaleString()} ${
|
|
398
|
+
seatsRemaining === 1 ? "seat" : "seats"
|
|
399
|
+
} remaining`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (seatsRemaining === 0) {
|
|
403
|
+
return "No seats remaining";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return `${Math.abs(seatsRemaining).toLocaleString()} over limit`;
|
|
407
|
+
}, [seatsRemaining]);
|
|
408
|
+
|
|
409
|
+
/*
|
|
410
|
+
* Total and mutually exclusive. isUserLimitBreached is tested first, so
|
|
411
|
+
* reaching the >= 90 test structurally guarantees the limit is not exceeded
|
|
412
|
+
* and the amber nudge can never co-fire with the red breach.
|
|
413
|
+
*
|
|
414
|
+
* The test runs on the displayed percent rather than the raw ratio so that
|
|
415
|
+
* the number triggering the warning is the number printed under the bar.
|
|
416
|
+
*/
|
|
417
|
+
const seatTone: SeatTone = useMemo(() => {
|
|
418
|
+
if (!licenseValid) {
|
|
419
|
+
return "healthy";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (isUserLimitBreached) {
|
|
423
|
+
return "breached";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (
|
|
427
|
+
typeof seatUsageDisplayPercent === "number" &&
|
|
428
|
+
seatUsageDisplayPercent >= SEAT_WARNING_PERCENT
|
|
429
|
+
) {
|
|
430
|
+
return "approaching";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return "healthy";
|
|
434
|
+
}, [licenseValid, isUserLimitBreached, seatUsageDisplayPercent]);
|
|
435
|
+
|
|
436
|
+
/*
|
|
437
|
+
* Copy containing apostrophes lives in string literals rather than JSX text,
|
|
438
|
+
* because react/no-unescaped-entities is enforced in this repo.
|
|
439
|
+
*/
|
|
440
|
+
const seatAdvisoryTitle: string = useMemo(() => {
|
|
441
|
+
if (seatTone === "breached") {
|
|
442
|
+
const over: number = Math.abs(seatsRemaining || 0);
|
|
443
|
+
|
|
444
|
+
return over === 1
|
|
445
|
+
? "1 user over your licensed seats"
|
|
446
|
+
: `${over.toLocaleString()} users over your licensed seats`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (seatTone === "approaching") {
|
|
450
|
+
if (seatsRemaining === 0) {
|
|
451
|
+
return "Every licensed seat is in use";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return seatsRemaining === 1
|
|
455
|
+
? "Only 1 seat left on your license"
|
|
456
|
+
: `Only ${(seatsRemaining || 0).toLocaleString()} seats left on your license`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return "";
|
|
460
|
+
}, [seatTone, seatsRemaining]);
|
|
461
|
+
|
|
462
|
+
const seatAdvisoryBody: string = useMemo(() => {
|
|
463
|
+
const limitText: string = (userLimit || 0).toLocaleString();
|
|
464
|
+
const inUse: string = `${(currentUserCount || 0).toLocaleString()} of ${limitText}`;
|
|
465
|
+
|
|
466
|
+
if (seatTone === "breached") {
|
|
467
|
+
return `This installation is using ${inUse} licensed seats. Expand your license so it covers everyone on the platform.`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (seatTone === "approaching") {
|
|
471
|
+
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.`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return "";
|
|
475
|
+
}, [seatTone, currentUserCount, userLimit]);
|
|
476
|
+
|
|
477
|
+
const seatUsageAriaValueText: string = useMemo(() => {
|
|
478
|
+
if (typeof seatUsageDisplayPercent !== "number") {
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (seatTone === "breached") {
|
|
483
|
+
return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatAdvisoryTitle}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (typeof seatsRemaining === "number") {
|
|
487
|
+
return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatsRemainingText}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `${seatUsageDisplayPercent}% of licensed seats in use`;
|
|
491
|
+
}, [
|
|
492
|
+
seatUsageDisplayPercent,
|
|
493
|
+
seatTone,
|
|
494
|
+
seatAdvisoryTitle,
|
|
495
|
+
seatsRemaining,
|
|
496
|
+
seatsRemainingText,
|
|
497
|
+
]);
|
|
498
|
+
|
|
328
499
|
const editionName: string = useMemo(() => {
|
|
329
500
|
if (!IS_ENTERPRISE_EDITION) {
|
|
330
501
|
return "Community Edition";
|
|
@@ -352,12 +523,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
352
523
|
return "bg-red-500";
|
|
353
524
|
}
|
|
354
525
|
|
|
355
|
-
if (
|
|
526
|
+
if (seatTone === "breached") {
|
|
356
527
|
return "bg-red-500";
|
|
357
528
|
}
|
|
358
529
|
|
|
530
|
+
if (seatTone === "approaching") {
|
|
531
|
+
return "bg-amber-500";
|
|
532
|
+
}
|
|
533
|
+
|
|
359
534
|
return "bg-emerald-500";
|
|
360
|
-
}, [isConfigLoading, licenseValid,
|
|
535
|
+
}, [isConfigLoading, licenseValid, seatTone]);
|
|
361
536
|
|
|
362
537
|
const ctaLabel: string = useMemo(() => {
|
|
363
538
|
if (!IS_ENTERPRISE_EDITION) {
|
|
@@ -372,13 +547,110 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
372
547
|
return "Validate license";
|
|
373
548
|
}
|
|
374
549
|
|
|
375
|
-
if (
|
|
550
|
+
if (seatTone === "breached") {
|
|
376
551
|
return "User limit exceeded";
|
|
377
552
|
}
|
|
378
553
|
|
|
554
|
+
if (seatTone === "approaching") {
|
|
555
|
+
return "Seats nearly full";
|
|
556
|
+
}
|
|
557
|
+
|
|
379
558
|
return "View details";
|
|
559
|
+
}, [isConfigLoading, licenseValid, seatTone]);
|
|
560
|
+
|
|
561
|
+
const pillTone: PillTone = useMemo(() => {
|
|
562
|
+
if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
|
|
563
|
+
return "normal";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (
|
|
567
|
+
seatTone === "breached" ||
|
|
568
|
+
(!licenseValid && Boolean(globalConfig?.enterpriseLicenseKey))
|
|
569
|
+
) {
|
|
570
|
+
return "alerted";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (seatTone === "approaching") {
|
|
574
|
+
return "warning";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return "normal";
|
|
578
|
+
}, [
|
|
579
|
+
isConfigLoading,
|
|
580
|
+
licenseValid,
|
|
581
|
+
seatTone,
|
|
582
|
+
globalConfig?.enterpriseLicenseKey,
|
|
583
|
+
]);
|
|
584
|
+
|
|
585
|
+
const modalIcon: IconProp = useMemo(() => {
|
|
586
|
+
if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
|
|
587
|
+
return IconProp.Cube;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!licenseValid || isUserLimitBreached) {
|
|
591
|
+
return IconProp.Alert;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return IconProp.ShieldCheck;
|
|
595
|
+
}, [isConfigLoading, licenseValid, isUserLimitBreached]);
|
|
596
|
+
|
|
597
|
+
const modalIconType: IconType = useMemo(() => {
|
|
598
|
+
if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
|
|
599
|
+
return IconType.Info;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!licenseValid || isUserLimitBreached) {
|
|
603
|
+
return IconType.Danger;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return IconType.Success;
|
|
380
607
|
}, [isConfigLoading, licenseValid, isUserLimitBreached]);
|
|
381
608
|
|
|
609
|
+
const modalDescription: string = useMemo(() => {
|
|
610
|
+
if (!IS_ENTERPRISE_EDITION) {
|
|
611
|
+
return "You are running the free, open-source build of OneUptime.";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (isConfigLoading) {
|
|
615
|
+
return "Checking your license with OneUptime...";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!licenseValid) {
|
|
619
|
+
return "Validate your license key to activate Enterprise Edition.";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return "License, seat usage, and the instances covered by this key.";
|
|
623
|
+
}, [isConfigLoading, licenseValid]);
|
|
624
|
+
|
|
625
|
+
/*
|
|
626
|
+
* The per-instance counts only exceed the unique total when someone actually
|
|
627
|
+
* appears on more than one instance, and the component cannot know whether
|
|
628
|
+
* they do — it receives per-instance counts and the unique total separately.
|
|
629
|
+
* So the summation is stated as a possibility, and is dropped entirely when
|
|
630
|
+
* there is no unique total to compare against. It names the unique user count
|
|
631
|
+
* rather than "the total above", which would be ambiguous next to a hero
|
|
632
|
+
* reading "119 / 120 seats".
|
|
633
|
+
*/
|
|
634
|
+
const instanceOverlapText: string = useMemo(() => {
|
|
635
|
+
const countingRule: string = `Seats are counted uniquely across all ${licenseInstances.length} instances that share this license — the same person on multiple instances uses one seat.`;
|
|
636
|
+
|
|
637
|
+
if (typeof currentUserCount !== "number") {
|
|
638
|
+
return countingRule;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return `${countingRule} Per-instance counts can therefore add up to more than the ${currentUserCount.toLocaleString()} unique ${
|
|
642
|
+
currentUserCount === 1 ? "user" : "users"
|
|
643
|
+
} counted above.`;
|
|
644
|
+
}, [licenseInstances.length, currentUserCount]);
|
|
645
|
+
|
|
646
|
+
const licenseKeyHelperText: string = useMemo(() => {
|
|
647
|
+
if (isChangingLicense) {
|
|
648
|
+
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.";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return "You have installed Enterprise Edition of OneUptime. You need to validate your license key. Need a license key? Contact our sales team at";
|
|
652
|
+
}, [isChangingLicense]);
|
|
653
|
+
|
|
382
654
|
const communityFeatures: Array<string> = useMemo(() => {
|
|
383
655
|
return [
|
|
384
656
|
"Full OneUptime platform with incident response, status pages, and workflow automation.",
|
|
@@ -440,6 +712,38 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
440
712
|
closeDialog();
|
|
441
713
|
};
|
|
442
714
|
|
|
715
|
+
type BuildSeatExpansionMailtoFunction = () => string;
|
|
716
|
+
|
|
717
|
+
const buildSeatExpansionMailto: BuildSeatExpansionMailtoFunction =
|
|
718
|
+
(): string => {
|
|
719
|
+
const subject: string =
|
|
720
|
+
seatTone === "breached"
|
|
721
|
+
? "Expand OneUptime license - over seat limit"
|
|
722
|
+
: "Expand OneUptime license - nearly out of seats";
|
|
723
|
+
|
|
724
|
+
const body: string = [
|
|
725
|
+
"Hi OneUptime,",
|
|
726
|
+
"",
|
|
727
|
+
"We would like to expand the seat count on our enterprise license.",
|
|
728
|
+
"",
|
|
729
|
+
`Company: ${globalConfig?.enterpriseCompanyName || "Not specified"}`,
|
|
730
|
+
`Seats in use: ${(currentUserCount || 0).toLocaleString()} of ${(
|
|
731
|
+
userLimit || 0
|
|
732
|
+
).toLocaleString()}`,
|
|
733
|
+
`Instances on this license: ${licenseInstances.length}`,
|
|
734
|
+
].join("\n");
|
|
735
|
+
|
|
736
|
+
return `${SALES_MAILTO_URL}?subject=${encodeURIComponent(
|
|
737
|
+
subject,
|
|
738
|
+
)}&body=${encodeURIComponent(body)}`;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const handleRequestMoreSeats: () => void = () => {
|
|
742
|
+
if (typeof window !== "undefined") {
|
|
743
|
+
window.location.href = buildSeatExpansionMailto();
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
443
747
|
const runLicenseValidation: (
|
|
444
748
|
key: string,
|
|
445
749
|
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
|
|
@@ -514,6 +818,27 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
514
818
|
|
|
515
819
|
const shouldShowEnterpriseValidationButton: boolean = showLicenseKeyInput;
|
|
516
820
|
|
|
821
|
+
/*
|
|
822
|
+
* Collapses the !configError && !isConfigLoading && licenseValid chain that
|
|
823
|
+
* gates the stat strip, the seat card and the instances card.
|
|
824
|
+
*/
|
|
825
|
+
const showLicenseDetails: boolean =
|
|
826
|
+
IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && licenseValid;
|
|
827
|
+
|
|
828
|
+
const showEnterpriseFeatureList: boolean =
|
|
829
|
+
IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && !licenseValid;
|
|
830
|
+
|
|
831
|
+
/*
|
|
832
|
+
* GlobalConfigAPI returns userLimit and currentUserCount to anonymous callers
|
|
833
|
+
* because the same route serves the signed-out login page, and gates only
|
|
834
|
+
* licenseKey, token, instances and instanceId on isAuthenticatedUser. Gate
|
|
835
|
+
* the sales ask on a field the server does redact, so a signed-out visitor is
|
|
836
|
+
* never shown an administrator-targeted CTA.
|
|
837
|
+
*/
|
|
838
|
+
const canSeeLicenseAdmin: boolean = Boolean(
|
|
839
|
+
globalConfig?.enterpriseLicenseKey,
|
|
840
|
+
);
|
|
841
|
+
|
|
517
842
|
const modalSubmitButtonText: string | undefined = IS_ENTERPRISE_EDITION
|
|
518
843
|
? shouldShowEnterpriseValidationButton
|
|
519
844
|
? "Validate License"
|
|
@@ -537,19 +862,56 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
537
862
|
: undefined
|
|
538
863
|
: false;
|
|
539
864
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
865
|
+
const modalRightElement: ReactElement | undefined = showLicenseDetails ? (
|
|
866
|
+
<span
|
|
867
|
+
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${
|
|
868
|
+
seatTone === "breached"
|
|
869
|
+
? "border-red-200 bg-red-50 text-red-700"
|
|
870
|
+
: seatTone === "approaching"
|
|
871
|
+
? "border-amber-200 bg-amber-50 text-amber-800"
|
|
872
|
+
: "border-emerald-200 bg-emerald-50 text-emerald-800"
|
|
873
|
+
}`}
|
|
874
|
+
>
|
|
875
|
+
<span
|
|
876
|
+
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
|
877
|
+
seatTone === "breached"
|
|
878
|
+
? "bg-red-500"
|
|
879
|
+
: seatTone === "approaching"
|
|
880
|
+
? "bg-amber-500"
|
|
881
|
+
: "bg-emerald-500"
|
|
882
|
+
}`}
|
|
883
|
+
/>
|
|
884
|
+
{seatTone === "breached"
|
|
885
|
+
? "Seat limit exceeded"
|
|
886
|
+
: seatTone === "approaching"
|
|
887
|
+
? "Seats nearly full"
|
|
888
|
+
: "License active"}
|
|
889
|
+
</span>
|
|
890
|
+
) : undefined;
|
|
891
|
+
|
|
892
|
+
const modalLeftFooterElement: ReactElement | undefined =
|
|
893
|
+
showLicenseDetails && !isChangingLicense ? (
|
|
894
|
+
<Button
|
|
895
|
+
title="Change license key"
|
|
896
|
+
icon={IconProp.Edit}
|
|
897
|
+
buttonStyle={ButtonStyleType.NORMAL}
|
|
898
|
+
onClick={handleStartChangingLicense}
|
|
899
|
+
/>
|
|
900
|
+
) : undefined;
|
|
901
|
+
|
|
902
|
+
const pillClassName: string =
|
|
903
|
+
pillTone === "alerted"
|
|
904
|
+
? "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"
|
|
905
|
+
: pillTone === "warning"
|
|
906
|
+
? "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"
|
|
907
|
+
: "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";
|
|
908
|
+
|
|
909
|
+
const pillCtaTextClassName: string =
|
|
910
|
+
pillTone === "alerted"
|
|
911
|
+
? "text-[11px] text-red-500 group-hover:text-red-600"
|
|
912
|
+
: pillTone === "warning"
|
|
913
|
+
? "text-[11px] text-amber-600 group-hover:text-amber-700"
|
|
914
|
+
: "text-[11px] text-indigo-500 group-hover:text-indigo-600";
|
|
553
915
|
|
|
554
916
|
return (
|
|
555
917
|
<>
|
|
@@ -564,11 +926,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
564
926
|
*/
|
|
565
927
|
aria-label={`${editionName}, ${ctaLabel}`}
|
|
566
928
|
>
|
|
567
|
-
{
|
|
929
|
+
{pillTone !== "normal" && (
|
|
568
930
|
<Icon
|
|
569
|
-
icon={
|
|
570
|
-
|
|
571
|
-
|
|
931
|
+
icon={
|
|
932
|
+
pillTone === "alerted"
|
|
933
|
+
? IconProp.Alert
|
|
934
|
+
: IconProp.ExclaimationCircle
|
|
935
|
+
}
|
|
936
|
+
className={`h-3 w-3 ${
|
|
937
|
+
pillTone === "alerted" ? "text-red-600" : "text-amber-600"
|
|
938
|
+
}`}
|
|
572
939
|
/>
|
|
573
940
|
)}
|
|
574
941
|
<span
|
|
@@ -581,353 +948,479 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
581
948
|
{isDialogOpen && (
|
|
582
949
|
<Modal
|
|
583
950
|
title={editionName}
|
|
951
|
+
description={modalDescription}
|
|
952
|
+
icon={modalIcon}
|
|
953
|
+
iconType={modalIconType}
|
|
954
|
+
rightElement={modalRightElement}
|
|
584
955
|
submitButtonText={modalSubmitButtonText}
|
|
585
956
|
closeButtonText="Close"
|
|
586
957
|
onClose={closeDialog}
|
|
587
958
|
onSubmit={modalOnSubmit}
|
|
588
|
-
modalWidth={ModalWidth.
|
|
959
|
+
modalWidth={ModalWidth.Medium}
|
|
589
960
|
isLoading={modalIsLoading}
|
|
590
961
|
disableSubmitButton={modalDisableSubmitButton}
|
|
591
962
|
isBodyLoading={IS_ENTERPRISE_EDITION ? isConfigLoading : false}
|
|
963
|
+
leftFooterElement={modalLeftFooterElement}
|
|
592
964
|
>
|
|
593
|
-
<div className="space-y-
|
|
965
|
+
<div className="space-y-4 text-sm text-gray-600">
|
|
594
966
|
{IS_ENTERPRISE_EDITION ? (
|
|
595
967
|
<>
|
|
968
|
+
{!configError && successMessage && (
|
|
969
|
+
<Alert type={AlertType.SUCCESS} title={successMessage} />
|
|
970
|
+
)}
|
|
971
|
+
|
|
596
972
|
{configError && (
|
|
597
|
-
<div className="rounded-
|
|
598
|
-
<
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
<div className="mt-3 -ml-3">
|
|
603
|
-
<Button
|
|
604
|
-
title="Retry"
|
|
605
|
-
buttonStyle={ButtonStyleType.DANGER}
|
|
606
|
-
onClick={handleRetryFetch}
|
|
607
|
-
isLoading={isConfigLoading}
|
|
973
|
+
<div className="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
974
|
+
<div className="flex items-start gap-2.5">
|
|
975
|
+
<Icon
|
|
976
|
+
icon={IconProp.Alert}
|
|
977
|
+
className="mt-0.5 h-4 w-4 shrink-0 text-red-600"
|
|
608
978
|
/>
|
|
979
|
+
<div className="min-w-0 flex-1">
|
|
980
|
+
<p className="text-sm font-semibold text-red-900">
|
|
981
|
+
Unable to load license details
|
|
982
|
+
</p>
|
|
983
|
+
<p className="mt-1 text-xs leading-relaxed text-red-800">
|
|
984
|
+
{configError}
|
|
985
|
+
</p>
|
|
986
|
+
<div className="mt-3">
|
|
987
|
+
<Button
|
|
988
|
+
title="Try again"
|
|
989
|
+
buttonStyle={ButtonStyleType.DANGER}
|
|
990
|
+
onClick={handleRetryFetch}
|
|
991
|
+
isLoading={isConfigLoading}
|
|
992
|
+
className="!mt-0 md:!ml-0"
|
|
993
|
+
/>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
609
996
|
</div>
|
|
610
997
|
</div>
|
|
611
998
|
)}
|
|
612
999
|
|
|
613
|
-
{
|
|
614
|
-
<
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
{
|
|
624
|
-
</
|
|
625
|
-
|
|
626
|
-
|
|
1000
|
+
{showLicenseDetails && (
|
|
1001
|
+
<dl className="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2">
|
|
1002
|
+
<div className="min-w-0 bg-white px-4 py-3">
|
|
1003
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
|
|
1004
|
+
Licensed to
|
|
1005
|
+
</dt>
|
|
1006
|
+
<dd
|
|
1007
|
+
className="mt-1 truncate text-sm font-medium text-gray-900"
|
|
1008
|
+
title={globalConfig?.enterpriseCompanyName || undefined}
|
|
1009
|
+
>
|
|
1010
|
+
{globalConfig?.enterpriseCompanyName || "Not specified"}
|
|
1011
|
+
</dd>
|
|
1012
|
+
</div>
|
|
1013
|
+
<div className="bg-white px-4 py-3">
|
|
1014
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
|
|
1015
|
+
Expires
|
|
1016
|
+
</dt>
|
|
1017
|
+
<dd className="mt-1 text-sm font-medium tabular-nums text-gray-900">
|
|
1018
|
+
{licenseExpiresAtText || "—"}
|
|
1019
|
+
</dd>
|
|
1020
|
+
</div>
|
|
1021
|
+
</dl>
|
|
627
1022
|
)}
|
|
628
1023
|
|
|
629
|
-
{
|
|
630
|
-
<
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
? "border-red-200 bg-red-50"
|
|
634
|
-
: "border-gray-200 bg-white"
|
|
635
|
-
}`}
|
|
1024
|
+
{showLicenseDetails && (
|
|
1025
|
+
<section
|
|
1026
|
+
aria-labelledby="edition-seats-heading"
|
|
1027
|
+
className="rounded-xl border border-gray-200 bg-white p-5"
|
|
636
1028
|
>
|
|
637
|
-
<div className="flex items-start gap-
|
|
638
|
-
<div
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
size={SizeProp.Regular}
|
|
650
|
-
/>
|
|
1029
|
+
<div className="flex items-start justify-between gap-4">
|
|
1030
|
+
<div className="min-w-0">
|
|
1031
|
+
<h4
|
|
1032
|
+
id="edition-seats-heading"
|
|
1033
|
+
className="text-sm font-semibold text-gray-900"
|
|
1034
|
+
>
|
|
1035
|
+
Licensed seats
|
|
1036
|
+
</h4>
|
|
1037
|
+
<p className="mt-0.5 text-xs text-gray-500">
|
|
1038
|
+
Unique users across every instance on this license.
|
|
1039
|
+
</p>
|
|
651
1040
|
</div>
|
|
652
|
-
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
</span>
|
|
667
|
-
)}
|
|
668
|
-
</div>
|
|
1041
|
+
{seatTone !== "healthy" && (
|
|
1042
|
+
<span
|
|
1043
|
+
className={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
|
1044
|
+
seatTone === "breached"
|
|
1045
|
+
? "bg-red-100 text-red-700"
|
|
1046
|
+
: "bg-amber-100 text-amber-800"
|
|
1047
|
+
}`}
|
|
1048
|
+
>
|
|
1049
|
+
{seatTone === "breached"
|
|
1050
|
+
? "Limit exceeded"
|
|
1051
|
+
: "Nearly full"}
|
|
1052
|
+
</span>
|
|
1053
|
+
)}
|
|
1054
|
+
</div>
|
|
669
1055
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1056
|
+
<div className="mt-4 flex items-end justify-between gap-4">
|
|
1057
|
+
<p className="flex items-baseline gap-1.5">
|
|
1058
|
+
<span
|
|
1059
|
+
className={`text-3xl font-semibold leading-none tabular-nums ${
|
|
1060
|
+
typeof currentUserCount !== "number"
|
|
1061
|
+
? "text-gray-300"
|
|
1062
|
+
: seatTone === "breached"
|
|
674
1063
|
? "text-red-700"
|
|
675
1064
|
: "text-gray-900"
|
|
1065
|
+
}`}
|
|
1066
|
+
>
|
|
1067
|
+
{typeof currentUserCount === "number"
|
|
1068
|
+
? currentUserCount.toLocaleString()
|
|
1069
|
+
: "—"}
|
|
1070
|
+
</span>
|
|
1071
|
+
<span className="text-sm tabular-nums text-gray-500">
|
|
1072
|
+
{" / "}
|
|
1073
|
+
{typeof userLimit === "number" && userLimit > 0
|
|
1074
|
+
? `${userLimit.toLocaleString()} seats`
|
|
1075
|
+
: "unlimited"}
|
|
1076
|
+
</span>
|
|
1077
|
+
</p>
|
|
1078
|
+
{typeof seatsRemaining === "number" && (
|
|
1079
|
+
<p
|
|
1080
|
+
className={`text-xs font-medium tabular-nums ${
|
|
1081
|
+
seatTone === "breached"
|
|
1082
|
+
? "text-red-700"
|
|
1083
|
+
: seatTone === "approaching"
|
|
1084
|
+
? "text-amber-700"
|
|
1085
|
+
: "text-gray-500"
|
|
1086
|
+
}`}
|
|
1087
|
+
>
|
|
1088
|
+
{seatsRemainingText}
|
|
1089
|
+
</p>
|
|
1090
|
+
)}
|
|
1091
|
+
</div>
|
|
1092
|
+
|
|
1093
|
+
{typeof seatUsageDisplayPercent === "number" && (
|
|
1094
|
+
<div className="mt-2.5">
|
|
1095
|
+
<div
|
|
1096
|
+
className="h-2.5 w-full overflow-hidden rounded-full bg-gray-100"
|
|
1097
|
+
role="progressbar"
|
|
1098
|
+
aria-label="Licensed seat usage"
|
|
1099
|
+
aria-valuenow={seatUsageBarPercent}
|
|
1100
|
+
aria-valuemin={0}
|
|
1101
|
+
aria-valuemax={100}
|
|
1102
|
+
aria-valuetext={seatUsageAriaValueText}
|
|
1103
|
+
>
|
|
1104
|
+
<div
|
|
1105
|
+
className={`h-full rounded-full transition-all duration-300 ease-out ${
|
|
1106
|
+
seatTone === "breached"
|
|
1107
|
+
? "bg-red-500"
|
|
1108
|
+
: seatTone === "approaching"
|
|
1109
|
+
? "bg-amber-500"
|
|
1110
|
+
: "bg-emerald-500"
|
|
676
1111
|
}`}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
? currentUserCount.toLocaleString()
|
|
680
|
-
: "—"}
|
|
681
|
-
</span>
|
|
682
|
-
<span className="text-sm text-gray-500">
|
|
683
|
-
{" / "}
|
|
684
|
-
{typeof userLimit === "number" && userLimit > 0
|
|
685
|
-
? `${userLimit.toLocaleString()} users`
|
|
686
|
-
: "unlimited"}
|
|
687
|
-
</span>
|
|
1112
|
+
style={{ width: `${seatUsageBarPercent}%` }}
|
|
1113
|
+
/>
|
|
688
1114
|
</div>
|
|
1115
|
+
<p className="mt-1.5 text-xs tabular-nums text-gray-500">
|
|
1116
|
+
{seatUsageDisplayPercent}% of licensed seats in use
|
|
1117
|
+
</p>
|
|
1118
|
+
</div>
|
|
1119
|
+
)}
|
|
689
1120
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1121
|
+
{seatTone !== "healthy" && canSeeLicenseAdmin && (
|
|
1122
|
+
<div
|
|
1123
|
+
role={seatTone === "breached" ? "alert" : "status"}
|
|
1124
|
+
className={`mt-4 rounded-lg border p-3.5 ${
|
|
1125
|
+
seatTone === "breached"
|
|
1126
|
+
? "border-red-200 bg-red-50"
|
|
1127
|
+
: "border-amber-200 bg-amber-50"
|
|
1128
|
+
}`}
|
|
1129
|
+
>
|
|
1130
|
+
<div className="flex items-start gap-2.5">
|
|
1131
|
+
{/*
|
|
1132
|
+
* Colour comes from className only. Never add a type
|
|
1133
|
+
* prop here: Icon emits both the type colour and
|
|
1134
|
+
* className into one class attribute, so two colour
|
|
1135
|
+
* classes would land on one svg and Tailwind's
|
|
1136
|
+
* emission order would pick the winner.
|
|
1137
|
+
*/}
|
|
1138
|
+
<Icon
|
|
1139
|
+
icon={
|
|
1140
|
+
seatTone === "breached"
|
|
1141
|
+
? IconProp.Alert
|
|
1142
|
+
: IconProp.ExclaimationCircle
|
|
1143
|
+
}
|
|
1144
|
+
className={`mt-0.5 h-4 w-4 shrink-0 ${
|
|
1145
|
+
seatTone === "breached"
|
|
1146
|
+
? "text-red-600"
|
|
1147
|
+
: "text-amber-600"
|
|
1148
|
+
}`}
|
|
1149
|
+
/>
|
|
1150
|
+
<div className="min-w-0 flex-1">
|
|
1151
|
+
<h5
|
|
1152
|
+
className={`text-sm font-semibold ${
|
|
1153
|
+
seatTone === "breached"
|
|
1154
|
+
? "text-red-900"
|
|
1155
|
+
: "text-amber-900"
|
|
1156
|
+
}`}
|
|
1157
|
+
>
|
|
1158
|
+
{seatAdvisoryTitle}
|
|
1159
|
+
</h5>
|
|
1160
|
+
<p
|
|
1161
|
+
className={`mt-1 text-xs leading-relaxed ${
|
|
1162
|
+
seatTone === "breached"
|
|
1163
|
+
? "text-red-800"
|
|
1164
|
+
: "text-amber-800"
|
|
1165
|
+
}`}
|
|
1166
|
+
>
|
|
1167
|
+
{seatAdvisoryBody}
|
|
1168
|
+
</p>
|
|
1169
|
+
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
1170
|
+
<Button
|
|
1171
|
+
title={
|
|
1172
|
+
seatTone === "breached"
|
|
1173
|
+
? "Expand your license"
|
|
1174
|
+
: "Request more seats"
|
|
1175
|
+
}
|
|
1176
|
+
icon={IconProp.Email}
|
|
1177
|
+
buttonStyle={
|
|
1178
|
+
seatTone === "breached"
|
|
1179
|
+
? ButtonStyleType.DANGER
|
|
1180
|
+
: ButtonStyleType.PRIMARY
|
|
1181
|
+
}
|
|
1182
|
+
onClick={handleRequestMoreSeats}
|
|
1183
|
+
className="!mt-0 md:!ml-0"
|
|
702
1184
|
/>
|
|
1185
|
+
<span
|
|
1186
|
+
className={`text-[11px] ${
|
|
1187
|
+
seatTone === "breached"
|
|
1188
|
+
? "text-red-700"
|
|
1189
|
+
: "text-amber-700"
|
|
1190
|
+
}`}
|
|
1191
|
+
>
|
|
1192
|
+
Or email{" "}
|
|
1193
|
+
<a
|
|
1194
|
+
href={SALES_MAILTO_URL}
|
|
1195
|
+
className={`font-medium underline ${
|
|
1196
|
+
seatTone === "breached"
|
|
1197
|
+
? "text-red-800 hover:text-red-900"
|
|
1198
|
+
: "text-amber-900 hover:text-amber-950"
|
|
1199
|
+
}`}
|
|
1200
|
+
>
|
|
1201
|
+
{SALES_EMAIL}
|
|
1202
|
+
</a>{" "}
|
|
1203
|
+
directly.
|
|
1204
|
+
</span>
|
|
703
1205
|
</div>
|
|
704
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
705
|
-
{userUsagePercent}% of licensed seats in use
|
|
706
|
-
</p>
|
|
707
1206
|
</div>
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
{isUserLimitBreached && (
|
|
711
|
-
<p className="mt-3 text-xs text-red-700">
|
|
712
|
-
Your installation has more users than your license
|
|
713
|
-
permits. Please contact{" "}
|
|
714
|
-
<a
|
|
715
|
-
href="mailto:sales@oneuptime.com"
|
|
716
|
-
className="font-medium text-red-800 underline hover:text-red-900"
|
|
717
|
-
>
|
|
718
|
-
sales@oneuptime.com
|
|
719
|
-
</a>{" "}
|
|
720
|
-
to expand your license.
|
|
721
|
-
</p>
|
|
722
|
-
)}
|
|
723
|
-
|
|
724
|
-
{licenseInstances.length > 1 && (
|
|
725
|
-
<p className="mt-3 text-xs text-gray-500">
|
|
726
|
-
Users are counted uniquely across all{" "}
|
|
727
|
-
{licenseInstances.length} instances that share this
|
|
728
|
-
license — the same user on multiple instances uses
|
|
729
|
-
one seat.
|
|
730
|
-
</p>
|
|
731
|
-
)}
|
|
732
|
-
|
|
733
|
-
<p className="mt-3 text-xs text-gray-500">
|
|
734
|
-
{userCountUpdatedAtText
|
|
735
|
-
? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
|
|
736
|
-
: "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours."}
|
|
737
|
-
</p>
|
|
1207
|
+
</div>
|
|
738
1208
|
</div>
|
|
1209
|
+
)}
|
|
1210
|
+
|
|
1211
|
+
<div className="mt-4 border-t border-gray-100 pt-3">
|
|
1212
|
+
<p className="text-xs text-gray-500">
|
|
1213
|
+
{userCountUpdatedAtText
|
|
1214
|
+
? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
|
|
1215
|
+
: "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours."}
|
|
1216
|
+
</p>
|
|
739
1217
|
</div>
|
|
740
|
-
</
|
|
1218
|
+
</section>
|
|
741
1219
|
)}
|
|
742
1220
|
|
|
743
|
-
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1221
|
+
{showLicenseDetails && licenseInstances.length > 0 && (
|
|
1222
|
+
<section
|
|
1223
|
+
aria-labelledby="edition-instances-heading"
|
|
1224
|
+
className="rounded-xl border border-gray-200 bg-white p-5"
|
|
1225
|
+
>
|
|
1226
|
+
<div className="flex items-center justify-between gap-3">
|
|
1227
|
+
<h4
|
|
1228
|
+
id="edition-instances-heading"
|
|
1229
|
+
className="text-sm font-semibold text-gray-900"
|
|
1230
|
+
>
|
|
1231
|
+
Instances on this license
|
|
1232
|
+
</h4>
|
|
1233
|
+
<span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium tabular-nums text-gray-600">
|
|
1234
|
+
{licenseInstances.length}{" "}
|
|
1235
|
+
{licenseInstances.length === 1
|
|
1236
|
+
? "instance"
|
|
1237
|
+
: "instances"}
|
|
1238
|
+
</span>
|
|
1239
|
+
</div>
|
|
1240
|
+
<p className="mt-1 text-xs leading-relaxed text-gray-500">
|
|
1241
|
+
Use the same license key on every instance you deploy
|
|
1242
|
+
(staging, production, and so on). Each instance reports
|
|
1243
|
+
its usage daily.
|
|
1244
|
+
</p>
|
|
1245
|
+
{licenseInstances.length > 1 && (
|
|
1246
|
+
<p className="mt-1.5 text-xs leading-relaxed text-gray-500">
|
|
1247
|
+
{instanceOverlapText}
|
|
763
1248
|
</p>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1249
|
+
)}
|
|
1250
|
+
<ul className="mt-3 divide-y divide-gray-100 overflow-hidden rounded-lg border border-gray-200">
|
|
1251
|
+
{licenseInstances.map(
|
|
1252
|
+
(
|
|
1253
|
+
instance: EnterpriseLicenseInstanceSummary,
|
|
1254
|
+
index: number,
|
|
1255
|
+
) => {
|
|
1256
|
+
const isThisInstance: boolean =
|
|
1257
|
+
Boolean(thisInstanceId) &&
|
|
1258
|
+
instance.instanceId === thisInstanceId;
|
|
1259
|
+
|
|
1260
|
+
return (
|
|
1261
|
+
<li
|
|
1262
|
+
key={instance.instanceId || index}
|
|
1263
|
+
className="flex items-center justify-between gap-4 bg-white px-3 py-2.5 transition-colors hover:bg-gray-50"
|
|
1264
|
+
>
|
|
1265
|
+
<div className="min-w-0 flex-1">
|
|
1266
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
1267
|
+
<span
|
|
1268
|
+
className="truncate text-sm font-medium text-gray-900"
|
|
1269
|
+
title={instance.host || "Unknown host"}
|
|
1270
|
+
>
|
|
781
1271
|
{instance.host || "Unknown host"}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
<p className="text-xs text-gray-500">
|
|
789
|
-
{formatInstanceReportedAt(
|
|
790
|
-
instance.lastReportedAt,
|
|
791
|
-
)}
|
|
792
|
-
</p>
|
|
793
|
-
</div>
|
|
794
|
-
<div className="shrink-0 text-right text-sm text-gray-700">
|
|
795
|
-
{typeof instance.userCount === "number"
|
|
796
|
-
? `${instance.userCount.toLocaleString()} ${
|
|
797
|
-
instance.userCount === 1
|
|
798
|
-
? "user"
|
|
799
|
-
: "users"
|
|
800
|
-
}`
|
|
801
|
-
: "—"}
|
|
1272
|
+
</span>
|
|
1273
|
+
{isThisInstance && (
|
|
1274
|
+
<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">
|
|
1275
|
+
This instance
|
|
1276
|
+
</span>
|
|
1277
|
+
)}
|
|
802
1278
|
</div>
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1279
|
+
<p className="mt-0.5 truncate text-xs text-gray-500">
|
|
1280
|
+
{formatInstanceReportedAt(
|
|
1281
|
+
instance.lastReportedAt,
|
|
1282
|
+
)}
|
|
1283
|
+
</p>
|
|
1284
|
+
</div>
|
|
1285
|
+
<div className="flex shrink-0 items-baseline justify-end gap-1">
|
|
1286
|
+
{typeof instance.userCount === "number" ? (
|
|
1287
|
+
<>
|
|
1288
|
+
<span className="text-sm font-medium tabular-nums text-gray-900">
|
|
1289
|
+
{instance.userCount.toLocaleString()}
|
|
1290
|
+
</span>
|
|
1291
|
+
<span className="text-xs text-gray-500">
|
|
1292
|
+
{instance.userCount === 1
|
|
1293
|
+
? "user"
|
|
1294
|
+
: "users"}
|
|
1295
|
+
</span>
|
|
1296
|
+
</>
|
|
1297
|
+
) : (
|
|
1298
|
+
<span className="text-sm text-gray-400">
|
|
1299
|
+
—
|
|
1300
|
+
</span>
|
|
1301
|
+
)}
|
|
1302
|
+
</div>
|
|
1303
|
+
</li>
|
|
1304
|
+
);
|
|
1305
|
+
},
|
|
1306
|
+
)}
|
|
1307
|
+
</ul>
|
|
1308
|
+
</section>
|
|
1309
|
+
)}
|
|
824
1310
|
|
|
825
1311
|
{!configError &&
|
|
826
1312
|
!isConfigLoading &&
|
|
827
1313
|
!licenseValid &&
|
|
828
1314
|
globalConfig?.enterpriseLicenseKey && (
|
|
829
|
-
<div className="rounded-
|
|
830
|
-
<p className="font-semibold">
|
|
1315
|
+
<div className="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
1316
|
+
<p className="text-sm font-semibold text-red-900">
|
|
831
1317
|
License validation required
|
|
832
1318
|
</p>
|
|
833
|
-
<p className="mt-1">
|
|
1319
|
+
<p className="mt-1 text-xs leading-relaxed text-red-800">
|
|
834
1320
|
The stored license information could not be verified.
|
|
835
1321
|
Please validate the license key again.
|
|
836
1322
|
</p>
|
|
837
1323
|
</div>
|
|
838
1324
|
)}
|
|
839
1325
|
|
|
840
|
-
{!configError && (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
<
|
|
1326
|
+
{!configError && showLicenseKeyInput && (
|
|
1327
|
+
<section className="rounded-xl border border-gray-200 bg-white p-5">
|
|
1328
|
+
<div className="flex items-start justify-between gap-4">
|
|
1329
|
+
<div className="min-w-0">
|
|
1330
|
+
<label
|
|
1331
|
+
htmlFor="enterprise-license-key"
|
|
1332
|
+
className="text-sm font-semibold text-gray-900"
|
|
1333
|
+
>
|
|
1334
|
+
{isChangingLicense
|
|
1335
|
+
? "New license key"
|
|
1336
|
+
: "License key"}
|
|
1337
|
+
</label>
|
|
1338
|
+
<p className="mt-0.5 text-xs leading-relaxed text-gray-500">
|
|
1339
|
+
{licenseKeyHelperText}
|
|
1340
|
+
{!isChangingLicense && (
|
|
1341
|
+
<>
|
|
1342
|
+
{" "}
|
|
1343
|
+
<a
|
|
1344
|
+
href={SALES_MAILTO_URL}
|
|
1345
|
+
className="font-medium text-indigo-600 hover:text-indigo-700"
|
|
1346
|
+
>
|
|
1347
|
+
{SALES_EMAIL}
|
|
1348
|
+
</a>
|
|
1349
|
+
.
|
|
1350
|
+
</>
|
|
1351
|
+
)}
|
|
1352
|
+
</p>
|
|
1353
|
+
</div>
|
|
1354
|
+
{isChangingLicense && (
|
|
1355
|
+
<Button
|
|
1356
|
+
title="Cancel"
|
|
1357
|
+
buttonStyle={ButtonStyleType.NORMAL}
|
|
1358
|
+
onClick={handleCancelChangingLicense}
|
|
1359
|
+
className="!mt-0 shrink-0 md:!ml-0"
|
|
1360
|
+
/>
|
|
1361
|
+
)}
|
|
1362
|
+
</div>
|
|
1363
|
+
<div className="mt-3">
|
|
1364
|
+
<Input
|
|
1365
|
+
id="enterprise-license-key"
|
|
1366
|
+
value={licenseKeyInput}
|
|
1367
|
+
onChange={(value: string) => {
|
|
1368
|
+
setLicenseKeyInput(value);
|
|
1369
|
+
licenseInputEditedRef.current = true;
|
|
1370
|
+
}}
|
|
1371
|
+
placeholder="Enter your enterprise license key"
|
|
1372
|
+
disableSpellCheck={true}
|
|
1373
|
+
/>
|
|
1374
|
+
</div>
|
|
1375
|
+
{validationError && (
|
|
1376
|
+
<div className="mt-3">
|
|
1377
|
+
<Alert
|
|
1378
|
+
type={AlertType.DANGER}
|
|
1379
|
+
title={validationError}
|
|
1380
|
+
/>
|
|
1381
|
+
</div>
|
|
844
1382
|
)}
|
|
1383
|
+
</section>
|
|
1384
|
+
)}
|
|
845
1385
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
type={AlertType.DANGER}
|
|
868
|
-
title={validationError}
|
|
869
|
-
/>
|
|
870
|
-
)}
|
|
871
|
-
|
|
872
|
-
{isChangingLicense ? (
|
|
873
|
-
<div className="flex items-center justify-between gap-3">
|
|
874
|
-
<p className="text-xs text-gray-500">
|
|
875
|
-
Enter the new enterprise license key and validate
|
|
876
|
-
it to replace the current one. Your existing
|
|
877
|
-
license stays active until the new key is
|
|
878
|
-
validated.
|
|
879
|
-
</p>
|
|
880
|
-
<Button
|
|
881
|
-
title="Cancel"
|
|
882
|
-
buttonStyle={ButtonStyleType.NORMAL}
|
|
883
|
-
onClick={handleCancelChangingLicense}
|
|
884
|
-
/>
|
|
885
|
-
</div>
|
|
886
|
-
) : (
|
|
887
|
-
<p className="text-xs text-gray-500">
|
|
888
|
-
You have installed Enterprise Edition of OneUptime.
|
|
889
|
-
You need to validate your license key. Need a
|
|
890
|
-
license key? Contact our sales team at{" "}
|
|
891
|
-
<a
|
|
892
|
-
href="mailto:sales@oneuptime.com"
|
|
893
|
-
className="font-medium text-indigo-600 hover:text-indigo-700"
|
|
1386
|
+
{showEnterpriseFeatureList && (
|
|
1387
|
+
<section
|
|
1388
|
+
aria-labelledby="edition-features-heading"
|
|
1389
|
+
className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-5"
|
|
1390
|
+
>
|
|
1391
|
+
<h4
|
|
1392
|
+
id="edition-features-heading"
|
|
1393
|
+
className="text-sm font-semibold text-indigo-900"
|
|
1394
|
+
>
|
|
1395
|
+
What your license unlocks
|
|
1396
|
+
</h4>
|
|
1397
|
+
<p className="mt-0.5 text-xs text-indigo-700">
|
|
1398
|
+
Validate your key above to turn these on immediately.
|
|
1399
|
+
</p>
|
|
1400
|
+
<ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
1401
|
+
{enterpriseFeatures.map(
|
|
1402
|
+
(feature: string, index: number) => {
|
|
1403
|
+
return (
|
|
1404
|
+
<li
|
|
1405
|
+
key={index}
|
|
1406
|
+
className="flex items-start gap-2 rounded-lg border border-gray-100 bg-white px-3 py-2.5"
|
|
894
1407
|
>
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1408
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-indigo-50">
|
|
1409
|
+
<Icon
|
|
1410
|
+
icon={IconProp.Check}
|
|
1411
|
+
className="h-3 w-3 text-indigo-600"
|
|
1412
|
+
/>
|
|
1413
|
+
</span>
|
|
1414
|
+
<span className="text-xs leading-snug text-gray-700">
|
|
1415
|
+
{feature}
|
|
1416
|
+
</span>
|
|
1417
|
+
</li>
|
|
1418
|
+
);
|
|
1419
|
+
},
|
|
1420
|
+
)}
|
|
1421
|
+
</ul>
|
|
1422
|
+
</section>
|
|
903
1423
|
)}
|
|
904
|
-
|
|
905
|
-
<div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm">
|
|
906
|
-
<h3 className="text-sm font-semibold text-indigo-900">
|
|
907
|
-
Enterprise Edition Features
|
|
908
|
-
</h3>
|
|
909
|
-
<ul className="mt-3 space-y-2 text-sm text-indigo-900">
|
|
910
|
-
{enterpriseFeatures.map(
|
|
911
|
-
(feature: string, index: number) => {
|
|
912
|
-
return (
|
|
913
|
-
<li key={index} className="flex items-start gap-2">
|
|
914
|
-
<Icon
|
|
915
|
-
icon={IconProp.Check}
|
|
916
|
-
type={IconType.Success}
|
|
917
|
-
size={SizeProp.Small}
|
|
918
|
-
className="mt-0.5"
|
|
919
|
-
/>
|
|
920
|
-
<span className="leading-snug">{feature}</span>
|
|
921
|
-
</li>
|
|
922
|
-
);
|
|
923
|
-
},
|
|
924
|
-
)}
|
|
925
|
-
</ul>
|
|
926
|
-
<p className="mt-3 text-xs text-indigo-700">
|
|
927
|
-
Already have a license? Validate it above to unlock these
|
|
928
|
-
premium capabilities immediately.
|
|
929
|
-
</p>
|
|
930
|
-
</div>
|
|
931
1424
|
</>
|
|
932
1425
|
) : (
|
|
933
1426
|
<>
|
|
@@ -937,10 +1430,10 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
937
1430
|
fit for your team.
|
|
938
1431
|
</p>
|
|
939
1432
|
<div className="grid gap-4 md:grid-cols-2">
|
|
940
|
-
<div className="rounded-
|
|
941
|
-
<
|
|
1433
|
+
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
|
1434
|
+
<h4 className="text-sm font-semibold text-gray-900">
|
|
942
1435
|
Community Edition
|
|
943
|
-
</
|
|
1436
|
+
</h4>
|
|
944
1437
|
<ul className="mt-3 space-y-2 text-sm text-gray-600">
|
|
945
1438
|
{communityFeatures.map(
|
|
946
1439
|
(feature: string, index: number) => {
|
|
@@ -948,8 +1441,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
948
1441
|
<li key={index} className="flex items-start gap-2">
|
|
949
1442
|
<Icon
|
|
950
1443
|
icon={IconProp.Check}
|
|
951
|
-
|
|
952
|
-
className="mt-0.5 text-gray-400"
|
|
1444
|
+
className="mt-0.5 h-3 w-3 shrink-0 text-gray-400"
|
|
953
1445
|
/>
|
|
954
1446
|
<span className="leading-snug">{feature}</span>
|
|
955
1447
|
</li>
|
|
@@ -962,10 +1454,10 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
962
1454
|
workflows.
|
|
963
1455
|
</p>
|
|
964
1456
|
</div>
|
|
965
|
-
<div className="rounded-
|
|
966
|
-
<
|
|
1457
|
+
<div className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-4">
|
|
1458
|
+
<h4 className="text-sm font-semibold text-indigo-900">
|
|
967
1459
|
Enterprise Edition
|
|
968
|
-
</
|
|
1460
|
+
</h4>
|
|
969
1461
|
<ul className="mt-3 space-y-2 text-sm text-indigo-900">
|
|
970
1462
|
{enterpriseFeatures.map(
|
|
971
1463
|
(feature: string, index: number) => {
|
|
@@ -973,9 +1465,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
973
1465
|
<li key={index} className="flex items-start gap-2">
|
|
974
1466
|
<Icon
|
|
975
1467
|
icon={IconProp.Check}
|
|
976
|
-
|
|
977
|
-
size={SizeProp.Small}
|
|
978
|
-
className="mt-0.5"
|
|
1468
|
+
className="mt-0.5 h-3 w-3 shrink-0 text-indigo-600"
|
|
979
1469
|
/>
|
|
980
1470
|
<span className="leading-snug">{feature}</span>
|
|
981
1471
|
</li>
|