@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.
- package/Models/DatabaseModels/AIRun.ts +31 -0
- package/Server/API/CodeFixRunAPI.ts +25 -74
- 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/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";
|
|
@@ -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
|
-
|
|
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.
|
|
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 (
|
|
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,
|
|
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 (
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
:
|
|
376
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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.
|
|
386
|
-
React.createElement("div", { className: "space-y-
|
|
387
|
-
configError && (React.createElement(
|
|
388
|
-
|
|
389
|
-
React.createElement("
|
|
390
|
-
|
|
391
|
-
React.createElement(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
React.createElement("
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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("
|
|
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("
|
|
470
|
-
instance.host || "Unknown host",
|
|
471
|
-
isThisInstance && (React.createElement("span", { className: "
|
|
472
|
-
React.createElement("p", { className: "text-xs text-gray-500" }, formatInstanceReportedAt(instance.lastReportedAt))),
|
|
473
|
-
React.createElement("div", { className: "shrink-0
|
|
474
|
-
|
|
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-
|
|
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(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
isChangingLicense
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
React.createElement("
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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-
|
|
520
|
-
React.createElement("
|
|
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,
|
|
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-
|
|
528
|
-
React.createElement("
|
|
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,
|
|
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."))),
|