@oneuptime/common 8.0.5496 → 8.0.5514

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 (47) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +104 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +71 -0
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Server/API/EnterpriseLicenseAPI.ts +102 -0
  5. package/Server/API/GlobalConfigAPI.ts +168 -0
  6. package/Server/EnvironmentConfig.ts +7 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.ts +69 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  9. package/Server/Services/EnterpriseLicenseService.ts +10 -0
  10. package/Server/Services/Index.ts +2 -0
  11. package/UI/Components/EditionLabel/EditionLabel.tsx +563 -0
  12. package/UI/Components/Footer/Footer.tsx +18 -2
  13. package/UI/Components/Link/Link.tsx +7 -4
  14. package/UI/Components/Modal/Modal.tsx +1 -1
  15. package/UI/Components/Modal/ModalFooter.tsx +2 -2
  16. package/UI/Config.ts +2 -0
  17. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +128 -0
  18. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -0
  19. package/build/dist/Models/DatabaseModels/GlobalConfig.js +78 -0
  20. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  21. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  22. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  23. package/build/dist/Server/API/EnterpriseLicenseAPI.js +71 -0
  24. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -0
  25. package/build/dist/Server/API/GlobalConfigAPI.js +129 -1
  26. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  27. package/build/dist/Server/EnvironmentConfig.js +2 -0
  28. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  29. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.js +30 -0
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.js.map +1 -0
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  33. package/build/dist/Server/Services/EnterpriseLicenseService.js +9 -0
  34. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -0
  35. package/build/dist/Server/Services/Index.js +2 -0
  36. package/build/dist/Server/Services/Index.js.map +1 -1
  37. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +302 -0
  38. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -0
  39. package/build/dist/UI/Components/Footer/Footer.js +6 -0
  40. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  41. package/build/dist/UI/Components/Link/Link.js +2 -1
  42. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  43. package/build/dist/UI/Components/Modal/ModalFooter.js +2 -1
  44. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  45. package/build/dist/UI/Config.js +1 -0
  46. package/build/dist/UI/Config.js.map +1 -1
  47. package/package.json +1 -1
@@ -0,0 +1,563 @@
1
+ import Modal, { ModalWidth } from "../Modal/Modal";
2
+ import Icon, { IconType, SizeProp } from "../Icon/Icon";
3
+ import IconProp from "../../../Types/Icon/IconProp";
4
+ import Input from "../Input/Input";
5
+ import Button, { ButtonStyleType } from "../Button/Button";
6
+ import React, {
7
+ FunctionComponent,
8
+ ReactElement,
9
+ useCallback,
10
+ useEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react";
15
+ import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
16
+ import API from "../../Utils/API/API";
17
+ import OneUptimeDate from "../../../Types/Date";
18
+ import HTTPMethod from "../../../Types/API/HTTPMethod";
19
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
20
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
21
+ import Route from "../../../Types/API/Route";
22
+ import URL from "../../../Types/API/URL";
23
+ import { JSONObject } from "../../../Types/JSON";
24
+ import {
25
+ APP_API_URL,
26
+ BILLING_ENABLED,
27
+ IS_ENTERPRISE_EDITION,
28
+ } from "../../Config";
29
+ import Alert, { AlertType } from "../Alerts/Alert";
30
+
31
+ export interface ComponentProps {
32
+ className?: string | undefined;
33
+ }
34
+
35
+ const ENTERPRISE_URL: string = "https://oneuptime.com/enterprise/demo";
36
+
37
+ const EditionLabel: FunctionComponent<ComponentProps> = (
38
+ props: ComponentProps,
39
+ ): ReactElement => {
40
+ const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
41
+ const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>(null);
42
+ const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
43
+ const [configError, setConfigError] = useState<string>("");
44
+ const [licenseKeyInput, setLicenseKeyInput] = useState<string>("");
45
+ const [validationError, setValidationError] = useState<string>("");
46
+ const [successMessage, setSuccessMessage] = useState<string>("");
47
+ const [isValidating, setIsValidating] = useState<boolean>(false);
48
+ const licenseInputEditedRef: React.MutableRefObject<boolean> =
49
+ useRef<boolean>(false);
50
+
51
+ if (BILLING_ENABLED) {
52
+ return <></>;
53
+ }
54
+
55
+ const fetchGlobalConfig: () => Promise<void> =
56
+ useCallback(async (): Promise<void> => {
57
+ if (!IS_ENTERPRISE_EDITION) {
58
+ return;
59
+ }
60
+
61
+ setIsConfigLoading(true);
62
+ setConfigError("");
63
+
64
+ try {
65
+ const licenseUrl: URL = URL.fromURL(APP_API_URL).addRoute(
66
+ new Route("/global-config/license"),
67
+ );
68
+
69
+ const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
70
+ await API.fetch<JSONObject>({
71
+ method: HTTPMethod.GET,
72
+ url: licenseUrl,
73
+ });
74
+
75
+ if (!response.isSuccess()) {
76
+ throw response;
77
+ }
78
+
79
+ const payload: JSONObject = response.data as JSONObject;
80
+
81
+ const configModel: GlobalConfig = new GlobalConfig();
82
+
83
+ if (payload["companyName"]) {
84
+ configModel.enterpriseCompanyName = payload["companyName"] as string;
85
+ }
86
+
87
+ if (payload["licenseKey"]) {
88
+ configModel.enterpriseLicenseKey = payload["licenseKey"] as string;
89
+ }
90
+
91
+ if (payload["token"]) {
92
+ configModel.enterpriseLicenseToken = payload["token"] as string;
93
+ }
94
+
95
+ if (payload["expiresAt"]) {
96
+ configModel.enterpriseLicenseExpiresAt = OneUptimeDate.fromString(
97
+ payload["expiresAt"] as string,
98
+ );
99
+ }
100
+
101
+ setGlobalConfig(configModel);
102
+
103
+ if (!licenseInputEditedRef.current) {
104
+ setLicenseKeyInput(configModel.enterpriseLicenseKey || "");
105
+ }
106
+ } catch (err) {
107
+ setGlobalConfig(null);
108
+ setConfigError(API.getFriendlyMessage(err));
109
+ } finally {
110
+ setIsConfigLoading(false);
111
+ }
112
+ }, []);
113
+
114
+ useEffect(() => {
115
+ void fetchGlobalConfig();
116
+ }, [fetchGlobalConfig]);
117
+
118
+ const licenseValid: boolean = useMemo(() => {
119
+ if (!IS_ENTERPRISE_EDITION) {
120
+ return false;
121
+ }
122
+
123
+ if (
124
+ !globalConfig?.enterpriseLicenseToken ||
125
+ !globalConfig.enterpriseLicenseExpiresAt
126
+ ) {
127
+ return false;
128
+ }
129
+
130
+ const expiresAt: Date = OneUptimeDate.fromString(
131
+ globalConfig.enterpriseLicenseExpiresAt,
132
+ );
133
+
134
+ return expiresAt.getTime() > Date.now();
135
+ }, [
136
+ globalConfig?.enterpriseLicenseExpiresAt,
137
+ globalConfig?.enterpriseLicenseToken,
138
+ ]);
139
+
140
+ const licenseExpiresAtText: string | null = useMemo(() => {
141
+ if (!globalConfig?.enterpriseLicenseExpiresAt) {
142
+ return null;
143
+ }
144
+
145
+ const expiresAt: Date = OneUptimeDate.fromString(
146
+ globalConfig.enterpriseLicenseExpiresAt,
147
+ );
148
+
149
+ if (Number.isNaN(expiresAt.getTime())) {
150
+ return null;
151
+ }
152
+
153
+ return expiresAt.toLocaleString();
154
+ }, [globalConfig?.enterpriseLicenseExpiresAt]);
155
+
156
+ const editionName: string = useMemo(() => {
157
+ if (!IS_ENTERPRISE_EDITION) {
158
+ return "Community Edition";
159
+ }
160
+
161
+ if (isConfigLoading) {
162
+ return "Enterprise Edition (Checking...)";
163
+ }
164
+
165
+ return licenseValid
166
+ ? "Enterprise Edition"
167
+ : "Enterprise Edition (License Required)";
168
+ }, [isConfigLoading, licenseValid]);
169
+
170
+ const indicatorColor: string = useMemo(() => {
171
+ if (!IS_ENTERPRISE_EDITION) {
172
+ return "bg-indigo-400";
173
+ }
174
+
175
+ if (isConfigLoading) {
176
+ return "bg-yellow-400";
177
+ }
178
+
179
+ return licenseValid ? "bg-emerald-500" : "bg-red-500";
180
+ }, [isConfigLoading, licenseValid]);
181
+
182
+ const ctaLabel: string = useMemo(() => {
183
+ if (!IS_ENTERPRISE_EDITION) {
184
+ return "Learn more";
185
+ }
186
+
187
+ if (isConfigLoading) {
188
+ return "Checking";
189
+ }
190
+
191
+ return licenseValid ? "View details" : "Validate license";
192
+ }, [isConfigLoading, licenseValid]);
193
+
194
+ const communityFeatures: Array<string> = useMemo(() => {
195
+ return [
196
+ "Full OneUptime platform with incident response, status pages, and workflow automation.",
197
+ "Community support, documentation, and tutorials to help teams get started quickly.",
198
+ "Regular updates, bug fixes, and open-source extensibility.",
199
+ "Integrations with popular DevOps tools through community-maintained connectors.",
200
+ ];
201
+ }, []);
202
+
203
+ const enterpriseFeatures: Array<string> = useMemo(() => {
204
+ return [
205
+ "Enterprise (hardened and secure) Docker images.",
206
+ "Dedicated enterprise support phone number available 24/7/365.",
207
+ "Priority chat and email support.",
208
+ "Dedicated engineer who can build custom features to integrate OneUptime with your ecosystem.",
209
+ "Compliance reports (ISO, SOC, GDPR, HIPAA).",
210
+ "Legal indemnification.",
211
+ "Audit logs and many more enterprise-focused features.",
212
+ ];
213
+ }, []);
214
+
215
+ const openDialog: () => void = () => {
216
+ setIsDialogOpen(true);
217
+ setValidationError("");
218
+ setSuccessMessage("");
219
+
220
+ if (IS_ENTERPRISE_EDITION) {
221
+ void fetchGlobalConfig();
222
+ }
223
+ };
224
+
225
+ const closeDialog: () => void = () => {
226
+ setIsDialogOpen(false);
227
+ setValidationError("");
228
+ setSuccessMessage("");
229
+ };
230
+
231
+ const handlePrimaryAction: () => void = () => {
232
+ if (typeof window !== "undefined") {
233
+ window.open(ENTERPRISE_URL, "_blank", "noopener,noreferrer");
234
+ }
235
+
236
+ closeDialog();
237
+ };
238
+
239
+ const runLicenseValidation: (
240
+ key: string,
241
+ setLoading: React.Dispatch<React.SetStateAction<boolean>>,
242
+ ) => Promise<void> = useCallback(
243
+ async (
244
+ key: string,
245
+ setLoading: React.Dispatch<React.SetStateAction<boolean>>,
246
+ ): Promise<void> => {
247
+ const trimmedKey: string = key.trim();
248
+
249
+ if (!trimmedKey) {
250
+ setValidationError("Please enter a license key before validating.");
251
+ setSuccessMessage("");
252
+ return;
253
+ }
254
+
255
+ setValidationError("");
256
+ setSuccessMessage("");
257
+ setLoading(true);
258
+
259
+ try {
260
+ const validationUrl: URL = URL.fromURL(APP_API_URL).addRoute(
261
+ new Route("/global-config/license"),
262
+ );
263
+
264
+ const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
265
+ await API.fetch<JSONObject>({
266
+ method: HTTPMethod.POST,
267
+ url: validationUrl,
268
+ data: {
269
+ licenseKey: trimmedKey,
270
+ },
271
+ });
272
+
273
+ if (!response.isSuccess()) {
274
+ throw response;
275
+ }
276
+
277
+ const payload: JSONObject = response.data as JSONObject;
278
+
279
+ licenseInputEditedRef.current = false;
280
+ setLicenseKeyInput((payload["licenseKey"] as string) || trimmedKey);
281
+ setSuccessMessage("License validated successfully.");
282
+
283
+ await fetchGlobalConfig();
284
+ } catch (err) {
285
+ setValidationError(API.getFriendlyMessage(err));
286
+ } finally {
287
+ setLoading(false);
288
+ }
289
+ },
290
+ [fetchGlobalConfig],
291
+ );
292
+
293
+ const handleValidateClick: () => void = () => {
294
+ if (isValidating) {
295
+ return;
296
+ }
297
+
298
+ void runLicenseValidation(licenseKeyInput, setIsValidating);
299
+ };
300
+
301
+ const handleRetryFetch: () => void = () => {
302
+ if (!isConfigLoading) {
303
+ void fetchGlobalConfig();
304
+ }
305
+ };
306
+
307
+ const shouldShowEnterpriseValidationButton: boolean =
308
+ IS_ENTERPRISE_EDITION && !licenseValid;
309
+
310
+ const modalSubmitButtonText: string | undefined = IS_ENTERPRISE_EDITION
311
+ ? shouldShowEnterpriseValidationButton
312
+ ? "Validate License"
313
+ : undefined
314
+ : "Talk to Sales";
315
+
316
+ const modalOnSubmit: (() => void) | undefined = IS_ENTERPRISE_EDITION
317
+ ? shouldShowEnterpriseValidationButton
318
+ ? handleValidateClick
319
+ : undefined
320
+ : handlePrimaryAction;
321
+
322
+ const modalIsLoading: boolean =
323
+ IS_ENTERPRISE_EDITION && shouldShowEnterpriseValidationButton
324
+ ? isValidating
325
+ : false;
326
+
327
+ const modalDisableSubmitButton: boolean | undefined = IS_ENTERPRISE_EDITION
328
+ ? shouldShowEnterpriseValidationButton
329
+ ? !licenseKeyInput.trim() || isValidating || isConfigLoading
330
+ : undefined
331
+ : false;
332
+
333
+ return (
334
+ <>
335
+ <button
336
+ type="button"
337
+ onClick={openDialog}
338
+ className={`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 ${
339
+ props.className ? props.className : ""
340
+ }`}
341
+ aria-label={`${editionName} details`}
342
+ >
343
+ <span
344
+ className={`h-2 w-2 rounded-full transition group-hover:scale-110 ${indicatorColor}`}
345
+ ></span>
346
+ <span className="tracking-wide">{editionName}</span>
347
+ <span className="text-[11px] text-indigo-500 group-hover:text-indigo-600">
348
+ {ctaLabel}
349
+ </span>
350
+ </button>
351
+
352
+ {isDialogOpen && (
353
+ <Modal
354
+ title={editionName}
355
+ submitButtonText={modalSubmitButtonText}
356
+ closeButtonText="Close"
357
+ onClose={closeDialog}
358
+ onSubmit={modalOnSubmit}
359
+ modalWidth={ModalWidth.Large}
360
+ isLoading={modalIsLoading}
361
+ disableSubmitButton={modalDisableSubmitButton}
362
+ isBodyLoading={IS_ENTERPRISE_EDITION ? isConfigLoading : false}
363
+ >
364
+ <div className="space-y-3 text-sm text-gray-600">
365
+ {IS_ENTERPRISE_EDITION ? (
366
+ <>
367
+ {configError && (
368
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
369
+ <p className="font-semibold">
370
+ Unable to load license details
371
+ </p>
372
+ <p className="mt-1">{configError}</p>
373
+ <div className="mt-3 -ml-3">
374
+ <Button
375
+ title="Retry"
376
+ buttonStyle={ButtonStyleType.DANGER}
377
+ onClick={handleRetryFetch}
378
+ isLoading={isConfigLoading}
379
+ />
380
+ </div>
381
+ </div>
382
+ )}
383
+
384
+ {!configError && !isConfigLoading && licenseValid && (
385
+ <div className="rounded-md border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">
386
+ <p className="font-semibold">License verified</p>
387
+ <p className="mt-1">
388
+ <span className="font-medium">Company:</span>{" "}
389
+ {globalConfig?.enterpriseCompanyName || "Not specified"}
390
+ </p>
391
+ {licenseExpiresAtText && (
392
+ <p>
393
+ <span className="font-medium">Expires:</span>{" "}
394
+ {licenseExpiresAtText}
395
+ </p>
396
+ )}
397
+ </div>
398
+ )}
399
+
400
+ {!configError &&
401
+ !isConfigLoading &&
402
+ !licenseValid &&
403
+ globalConfig?.enterpriseLicenseKey && (
404
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
405
+ <p className="font-semibold">
406
+ License validation required
407
+ </p>
408
+ <p className="mt-1">
409
+ The stored license information could not be verified.
410
+ Please validate the license key again.
411
+ </p>
412
+ </div>
413
+ )}
414
+
415
+ {!configError && (
416
+ <>
417
+ {successMessage && (
418
+ <Alert type={AlertType.SUCCESS} title={successMessage} />
419
+ )}
420
+
421
+ {!licenseValid && (
422
+ <>
423
+ <div>
424
+ <label className="text-sm font-medium text-gray-700">
425
+ License Key
426
+ </label>
427
+ <Input
428
+ value={licenseKeyInput}
429
+ onChange={(value: string) => {
430
+ setLicenseKeyInput(value);
431
+ licenseInputEditedRef.current = true;
432
+ }}
433
+ placeholder="Enter your enterprise license key"
434
+ disableSpellCheck={true}
435
+ />
436
+ </div>
437
+
438
+ {validationError && (
439
+ <Alert
440
+ type={AlertType.DANGER}
441
+ title={validationError}
442
+ />
443
+ )}
444
+
445
+ <p className="text-xs text-gray-500">
446
+ You have installed Enterprise Edition of OneUptime.
447
+ You need to validate your license key. Need a license
448
+ key? Contact our sales team at{" "}
449
+ <a
450
+ href="mailto:sales@oneuptime.com"
451
+ className="font-medium text-indigo-600 hover:text-indigo-700"
452
+ >
453
+ sales@oneuptime.com
454
+ </a>
455
+ .
456
+ </p>
457
+ </>
458
+ )}
459
+ </>
460
+ )}
461
+
462
+ <div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm">
463
+ <h3 className="text-sm font-semibold text-indigo-900">
464
+ Enterprise Edition Features
465
+ </h3>
466
+ <ul className="mt-3 space-y-2 text-sm text-indigo-900">
467
+ {enterpriseFeatures.map(
468
+ (feature: string, index: number) => {
469
+ return (
470
+ <li key={index} className="flex items-start gap-2">
471
+ <Icon
472
+ icon={IconProp.Check}
473
+ type={IconType.Success}
474
+ size={SizeProp.Small}
475
+ className="mt-0.5"
476
+ />
477
+ <span className="leading-snug">{feature}</span>
478
+ </li>
479
+ );
480
+ },
481
+ )}
482
+ </ul>
483
+ <p className="mt-3 text-xs text-indigo-700">
484
+ Already have a license? Validate it above to unlock these
485
+ premium capabilities immediately.
486
+ </p>
487
+ </div>
488
+ </>
489
+ ) : (
490
+ <>
491
+ <p>
492
+ You are running the Community Edition of OneUptime. Here is a
493
+ quick comparison to help you decide if Enterprise is the right
494
+ fit for your team.
495
+ </p>
496
+ <div className="grid gap-4 md:grid-cols-2">
497
+ <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
498
+ <h3 className="text-sm font-semibold text-gray-900">
499
+ Community Edition
500
+ </h3>
501
+ <ul className="mt-3 space-y-2 text-sm text-gray-600">
502
+ {communityFeatures.map(
503
+ (feature: string, index: number) => {
504
+ return (
505
+ <li key={index} className="flex items-start gap-2">
506
+ <Icon
507
+ icon={IconProp.Check}
508
+ size={SizeProp.Small}
509
+ className="mt-0.5 text-gray-400"
510
+ />
511
+ <span className="leading-snug">{feature}</span>
512
+ </li>
513
+ );
514
+ },
515
+ )}
516
+ </ul>
517
+ <p className="mt-3 text-xs text-gray-500">
518
+ Best for small teams experimenting with reliability
519
+ workflows.
520
+ </p>
521
+ </div>
522
+ <div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm">
523
+ <h3 className="text-sm font-semibold text-indigo-900">
524
+ Enterprise Edition
525
+ </h3>
526
+ <ul className="mt-3 space-y-2 text-sm text-indigo-900">
527
+ {enterpriseFeatures.map(
528
+ (feature: string, index: number) => {
529
+ return (
530
+ <li key={index} className="flex items-start gap-2">
531
+ <Icon
532
+ icon={IconProp.Check}
533
+ type={IconType.Success}
534
+ size={SizeProp.Small}
535
+ className="mt-0.5"
536
+ />
537
+ <span className="leading-snug">{feature}</span>
538
+ </li>
539
+ );
540
+ },
541
+ )}
542
+ </ul>
543
+ <p className="mt-3 text-xs text-indigo-700">
544
+ Everything in Community plus white-glove onboarding,
545
+ enterprise SLAs, and a partner dedicated to your
546
+ reliability goals.
547
+ </p>
548
+ </div>
549
+ </div>
550
+ <p className="text-xs text-gray-500">
551
+ Ready to unlock enterprise capabilities? Click &quot;Talk to
552
+ Sales&quot; to start the conversation.
553
+ </p>
554
+ </>
555
+ )}
556
+ </div>
557
+ </Modal>
558
+ )}
559
+ </>
560
+ );
561
+ };
562
+
563
+ export default EditionLabel;
@@ -1,13 +1,14 @@
1
1
  import UILink from "../Link/Link";
2
2
  import Route from "../../../Types/API/Route";
3
3
  import URL from "../../../Types/API/URL";
4
- import React, { FunctionComponent, ReactElement } from "react";
4
+ import React, { FunctionComponent, ReactElement, ReactNode } from "react";
5
5
 
6
6
  export interface FooterLink {
7
7
  onClick?: (() => void) | undefined;
8
8
  openInNewTab?: boolean | undefined;
9
9
  to?: Route | URL | undefined;
10
- title: string;
10
+ title?: ReactNode;
11
+ content?: ReactNode;
11
12
  }
12
13
 
13
14
  export interface ComponentProps {
@@ -32,6 +33,21 @@ const Footer: FunctionComponent<ComponentProps> = (
32
33
  {props.links &&
33
34
  props.links.length > 0 &&
34
35
  props.links.map((link: FooterLink, i: number) => {
36
+ if (link.content) {
37
+ return (
38
+ <div
39
+ key={i}
40
+ className="text-gray-400 hover:text-gray-500 text-center md:text-left"
41
+ >
42
+ {link.content}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ if (!link.title) {
48
+ return <React.Fragment key={i}></React.Fragment>;
49
+ }
50
+
35
51
  return (
36
52
  <UILink
37
53
  key={i}
@@ -2,10 +2,10 @@ import Navigation from "../../Utils/Navigation";
2
2
  import Route from "../../../Types/API/Route";
3
3
  import URL from "../../../Types/API/URL";
4
4
  import { JSONObject } from "../../../Types/JSON";
5
- import React, { FunctionComponent, ReactElement } from "react";
5
+ import React, { FunctionComponent, ReactElement, ReactNode } from "react";
6
6
 
7
7
  export interface ComponentProps {
8
- children: ReactElement | Array<ReactElement> | string;
8
+ children: ReactNode;
9
9
  className?: undefined | string;
10
10
  to?: Route | URL | null | undefined;
11
11
  onClick?: undefined | (() => void);
@@ -22,9 +22,12 @@ export interface ComponentProps {
22
22
  const Link: FunctionComponent<ComponentProps> = (
23
23
  props: ComponentProps,
24
24
  ): ReactElement => {
25
- let children: ReactElement | Array<ReactElement>;
25
+ let children: ReactNode;
26
26
 
27
- if (typeof props.children === "string") {
27
+ if (
28
+ typeof props.children === "string" ||
29
+ typeof props.children === "number"
30
+ ) {
28
31
  children = <span>{props.children}</span>;
29
32
  } else {
30
33
  children = props.children;
@@ -20,7 +20,7 @@ export interface ComponentProps {
20
20
  children: Array<ReactElement> | ReactElement;
21
21
  onClose?: undefined | (() => void);
22
22
  submitButtonText?: undefined | string;
23
- onSubmit: () => void;
23
+ onSubmit?: (() => void) | undefined;
24
24
  submitButtonStyleType?: undefined | ButtonStyleType;
25
25
  submitButtonType?: undefined | ButtonType;
26
26
  closeButtonStyleType?: undefined | ButtonStyleType;
@@ -5,7 +5,7 @@ import React, { FunctionComponent, ReactElement } from "react";
5
5
  export interface ComponentProps {
6
6
  onClose?: undefined | (() => void) | undefined;
7
7
  submitButtonText?: undefined | string;
8
- onSubmit: () => void;
8
+ onSubmit?: (() => void) | undefined;
9
9
  submitButtonStyleType?: undefined | ButtonStyleType;
10
10
  closeButtonStyleType?: undefined | ButtonStyleType;
11
11
  submitButtonType?: undefined | ButtonType;
@@ -30,7 +30,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
30
30
  props.submitButtonText ? props.submitButtonText : "Save Changes"
31
31
  }
32
32
  onClick={() => {
33
- props.onSubmit();
33
+ props.onSubmit?.();
34
34
  }}
35
35
  disabled={props.disableSubmitButton || false}
36
36
  isLoading={props.isLoading || false}
package/UI/Config.ts CHANGED
@@ -48,6 +48,8 @@ export const HTTP_PROTOCOL: Protocol =
48
48
  export const HOST: string = env("HOST") || "";
49
49
 
50
50
  export const BILLING_ENABLED: boolean = env("BILLING_ENABLED") === "true";
51
+ export const IS_ENTERPRISE_EDITION: boolean =
52
+ env("IS_ENTERPRISE_EDITION") === "true";
51
53
  export const BILLING_PUBLIC_KEY: string = env("BILLING_PUBLIC_KEY") || "";
52
54
 
53
55
  // VAPID Configuration for Push Notifications