@oneuptime/common 8.0.5493 → 8.0.5513

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 (58) 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/IncidentService.ts +13 -2
  11. package/Server/Services/Index.ts +2 -0
  12. package/UI/Components/EditionLabel/EditionLabel.tsx +545 -0
  13. package/UI/Components/Footer/Footer.tsx +18 -2
  14. package/UI/Components/Link/Link.tsx +7 -4
  15. package/UI/Components/LogsViewer/LogsViewer.tsx +4 -0
  16. package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +46 -0
  17. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +4 -0
  18. package/UI/Components/LogsViewer/types.ts +5 -0
  19. package/UI/Config.ts +2 -0
  20. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +128 -0
  21. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -0
  22. package/build/dist/Models/DatabaseModels/GlobalConfig.js +78 -0
  23. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  24. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  25. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  26. package/build/dist/Server/API/EnterpriseLicenseAPI.js +71 -0
  27. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -0
  28. package/build/dist/Server/API/GlobalConfigAPI.js +129 -1
  29. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  30. package/build/dist/Server/EnvironmentConfig.js +2 -0
  31. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.js +30 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.js.map +1 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/EnterpriseLicenseService.js +9 -0
  37. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -0
  38. package/build/dist/Server/Services/IncidentService.js +5 -4
  39. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  40. package/build/dist/Server/Services/Index.js +2 -0
  41. package/build/dist/Server/Services/Index.js.map +1 -1
  42. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +285 -0
  43. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -0
  44. package/build/dist/UI/Components/Footer/Footer.js +6 -0
  45. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  46. package/build/dist/UI/Components/Link/Link.js +2 -1
  47. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  48. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +2 -5
  49. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  50. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +22 -0
  51. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -0
  52. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +4 -1
  53. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  54. package/build/dist/UI/Components/LogsViewer/types.js +2 -0
  55. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -0
  56. package/build/dist/UI/Config.js +1 -0
  57. package/build/dist/UI/Config.js.map +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,545 @@
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
+ return (
308
+ <>
309
+ <button
310
+ type="button"
311
+ onClick={openDialog}
312
+ 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 ${
313
+ props.className ? props.className : ""
314
+ }`}
315
+ aria-label={`${editionName} details`}
316
+ >
317
+ <span
318
+ className={`h-2 w-2 rounded-full transition group-hover:scale-110 ${indicatorColor}`}
319
+ ></span>
320
+ <span className="tracking-wide">{editionName}</span>
321
+ <span className="text-[11px] text-indigo-500 group-hover:text-indigo-600">
322
+ {ctaLabel}
323
+ </span>
324
+ </button>
325
+
326
+ {isDialogOpen && (
327
+ <Modal
328
+ title={editionName}
329
+ submitButtonText={
330
+ IS_ENTERPRISE_EDITION ? "Validate License" : "Talk to Sales"
331
+ }
332
+ closeButtonText="Close"
333
+ onClose={closeDialog}
334
+ onSubmit={
335
+ IS_ENTERPRISE_EDITION ? handleValidateClick : handlePrimaryAction
336
+ }
337
+ modalWidth={ModalWidth.Large}
338
+ isLoading={IS_ENTERPRISE_EDITION ? isValidating : false}
339
+ disableSubmitButton={
340
+ IS_ENTERPRISE_EDITION
341
+ ? !licenseKeyInput.trim() || isValidating || isConfigLoading
342
+ : false
343
+ }
344
+ isBodyLoading={IS_ENTERPRISE_EDITION ? isConfigLoading : false}
345
+ >
346
+ <div className="space-y-3 text-sm text-gray-600">
347
+ {IS_ENTERPRISE_EDITION ? (
348
+ <>
349
+ {configError && (
350
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
351
+ <p className="font-semibold">
352
+ Unable to load license details
353
+ </p>
354
+ <p className="mt-1">{configError}</p>
355
+ <div className="mt-3 -ml-3">
356
+ <Button
357
+ title="Retry"
358
+ buttonStyle={ButtonStyleType.DANGER}
359
+ onClick={handleRetryFetch}
360
+ isLoading={isConfigLoading}
361
+ />
362
+ </div>
363
+ </div>
364
+ )}
365
+
366
+ {!configError && !isConfigLoading && licenseValid && (
367
+ <div className="rounded-md border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-700">
368
+ <p className="font-semibold">License verified</p>
369
+ <p className="mt-1">
370
+ <span className="font-medium">Company:</span>{" "}
371
+ {globalConfig?.enterpriseCompanyName || "Not specified"}
372
+ </p>
373
+ {licenseExpiresAtText && (
374
+ <p>
375
+ <span className="font-medium">Expires:</span>{" "}
376
+ {licenseExpiresAtText}
377
+ </p>
378
+ )}
379
+ </div>
380
+ )}
381
+
382
+ {!configError &&
383
+ !isConfigLoading &&
384
+ !licenseValid &&
385
+ globalConfig?.enterpriseLicenseKey && (
386
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
387
+ <p className="font-semibold">
388
+ License validation required
389
+ </p>
390
+ <p className="mt-1">
391
+ The stored license information could not be verified.
392
+ Please validate the license key again.
393
+ </p>
394
+ </div>
395
+ )}
396
+
397
+ {!configError && (
398
+ <>
399
+ {successMessage && (
400
+ <Alert type={AlertType.SUCCESS} title={successMessage} />
401
+ )}
402
+
403
+ {!licenseValid && (
404
+ <>
405
+ <div>
406
+ <label className="text-sm font-medium text-gray-700">
407
+ License Key
408
+ </label>
409
+ <Input
410
+ value={licenseKeyInput}
411
+ onChange={(value: string) => {
412
+ setLicenseKeyInput(value);
413
+ licenseInputEditedRef.current = true;
414
+ }}
415
+ placeholder="Enter your enterprise license key"
416
+ disableSpellCheck={true}
417
+ />
418
+ </div>
419
+
420
+ {validationError && (
421
+ <Alert
422
+ type={AlertType.DANGER}
423
+ title={validationError}
424
+ />
425
+ )}
426
+
427
+ <p className="text-xs text-gray-500">
428
+ You have installed Enterprise Edition of OneUptime.
429
+ You need to validate your license key. Need a license
430
+ key? Contact our sales team at{" "}
431
+ <a
432
+ href="mailto:sales@oneuptime.com"
433
+ className="font-medium text-indigo-600 hover:text-indigo-700"
434
+ >
435
+ sales@oneuptime.com
436
+ </a>
437
+ .
438
+ </p>
439
+ </>
440
+ )}
441
+ </>
442
+ )}
443
+
444
+ <div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm">
445
+ <h3 className="text-sm font-semibold text-indigo-900">
446
+ Enterprise Edition Features
447
+ </h3>
448
+ <ul className="mt-3 space-y-2 text-sm text-indigo-900">
449
+ {enterpriseFeatures.map(
450
+ (feature: string, index: number) => {
451
+ return (
452
+ <li key={index} className="flex items-start gap-2">
453
+ <Icon
454
+ icon={IconProp.Check}
455
+ type={IconType.Success}
456
+ size={SizeProp.Small}
457
+ className="mt-0.5"
458
+ />
459
+ <span className="leading-snug">{feature}</span>
460
+ </li>
461
+ );
462
+ },
463
+ )}
464
+ </ul>
465
+ <p className="mt-3 text-xs text-indigo-700">
466
+ Already have a license? Validate it above to unlock these
467
+ premium capabilities immediately.
468
+ </p>
469
+ </div>
470
+ </>
471
+ ) : (
472
+ <>
473
+ <p>
474
+ You are running the Community Edition of OneUptime. Here is a
475
+ quick comparison to help you decide if Enterprise is the right
476
+ fit for your team.
477
+ </p>
478
+ <div className="grid gap-4 md:grid-cols-2">
479
+ <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
480
+ <h3 className="text-sm font-semibold text-gray-900">
481
+ Community Edition
482
+ </h3>
483
+ <ul className="mt-3 space-y-2 text-sm text-gray-600">
484
+ {communityFeatures.map(
485
+ (feature: string, index: number) => {
486
+ return (
487
+ <li key={index} className="flex items-start gap-2">
488
+ <Icon
489
+ icon={IconProp.Check}
490
+ size={SizeProp.Small}
491
+ className="mt-0.5 text-gray-400"
492
+ />
493
+ <span className="leading-snug">{feature}</span>
494
+ </li>
495
+ );
496
+ },
497
+ )}
498
+ </ul>
499
+ <p className="mt-3 text-xs text-gray-500">
500
+ Best for small teams experimenting with reliability
501
+ workflows.
502
+ </p>
503
+ </div>
504
+ <div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4 shadow-sm">
505
+ <h3 className="text-sm font-semibold text-indigo-900">
506
+ Enterprise Edition
507
+ </h3>
508
+ <ul className="mt-3 space-y-2 text-sm text-indigo-900">
509
+ {enterpriseFeatures.map(
510
+ (feature: string, index: number) => {
511
+ return (
512
+ <li key={index} className="flex items-start gap-2">
513
+ <Icon
514
+ icon={IconProp.Check}
515
+ type={IconType.Success}
516
+ size={SizeProp.Small}
517
+ className="mt-0.5"
518
+ />
519
+ <span className="leading-snug">{feature}</span>
520
+ </li>
521
+ );
522
+ },
523
+ )}
524
+ </ul>
525
+ <p className="mt-3 text-xs text-indigo-700">
526
+ Everything in Community plus white-glove onboarding,
527
+ enterprise SLAs, and a partner dedicated to your
528
+ reliability goals.
529
+ </p>
530
+ </div>
531
+ </div>
532
+ <p className="text-xs text-gray-500">
533
+ Ready to unlock enterprise capabilities? Click &quot;Talk to
534
+ Sales&quot; to start the conversation.
535
+ </p>
536
+ </>
537
+ )}
538
+ </div>
539
+ </Modal>
540
+ )}
541
+ </>
542
+ );
543
+ };
544
+
545
+ 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;
@@ -34,6 +34,7 @@ import LogsTable, {
34
34
  } from "./components/LogsTable";
35
35
  import LogsPagination from "./components/LogsPagination";
36
36
  import LogDetailsPanel from "./components/LogDetailsPanel";
37
+ import { LiveLogsOptions } from "./types";
37
38
 
38
39
  export interface ComponentProps {
39
40
  logs: Array<Log>;
@@ -52,9 +53,11 @@ export interface ComponentProps {
52
53
  sortField?: LogsTableSortField | undefined;
53
54
  sortOrder?: SortOrder | undefined;
54
55
  onSortChange?: (field: LogsTableSortField, order: SortOrder) => void;
56
+ liveOptions?: LiveLogsOptions | undefined;
55
57
  }
56
58
 
57
59
  export type LogsSortField = LogsTableSortField;
60
+ export type { LiveLogsOptions } from "./types";
58
61
 
59
62
  const DEFAULT_PAGE_SIZE: number = 100;
60
63
  const PAGE_SIZE_OPTIONS: Array<number> = [100, 250, 500, 1000];
@@ -389,6 +392,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
389
392
  resultCount: totalItems,
390
393
  currentPage,
391
394
  totalPages,
395
+ ...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
392
396
  };
393
397
 
394
398
  return (
@@ -0,0 +1,46 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+ import { LiveLogsOptions } from "../types";
3
+
4
+ export type LiveLogsToggleProps = LiveLogsOptions;
5
+
6
+ const LiveLogsToggle: FunctionComponent<LiveLogsToggleProps> = (
7
+ props: LiveLogsToggleProps,
8
+ ): ReactElement => {
9
+ const { isLive, onToggle, isDisabled } = props;
10
+
11
+ const baseClasses: string =
12
+ "inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-emerald-500/60 bg-white/90 backdrop-blur";
13
+ const activeClasses: string = isLive
14
+ ? "border-emerald-600 text-emerald-700 hover:border-emerald-500 hover:bg-emerald-50"
15
+ : "border-slate-300 text-slate-600 hover:border-slate-400 hover:bg-white";
16
+ const disabledClasses: string = isDisabled
17
+ ? "cursor-not-allowed opacity-50"
18
+ : "cursor-pointer";
19
+
20
+ const content: ReactElement = (
21
+ <button
22
+ type="button"
23
+ aria-pressed={isLive}
24
+ disabled={isDisabled}
25
+ onClick={() => {
26
+ if (isDisabled) {
27
+ return;
28
+ }
29
+
30
+ onToggle(!isLive);
31
+ }}
32
+ className={`${baseClasses} ${activeClasses} ${disabledClasses}`}
33
+ >
34
+ <span
35
+ className={`h-2 w-2 rounded-full ${
36
+ isLive ? "bg-emerald-500 animate-pulse" : "bg-slate-400"
37
+ }`}
38
+ />
39
+ <span className="font-semibold">Live</span>
40
+ </button>
41
+ );
42
+
43
+ return content;
44
+ };
45
+
46
+ export default LiveLogsToggle;