@oneuptime/common 8.0.5496 → 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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +104 -0
- package/Models/DatabaseModels/GlobalConfig.ts +71 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +102 -0
- package/Server/API/GlobalConfigAPI.ts +168 -0
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/EnterpriseLicenseService.ts +10 -0
- package/Server/Services/Index.ts +2 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +545 -0
- package/UI/Components/Footer/Footer.tsx +18 -2
- package/UI/Components/Link/Link.tsx +7 -4
- package/UI/Config.ts +2 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +128 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +78 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +71 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -0
- package/build/dist/Server/API/GlobalConfigAPI.js +129 -1
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +2 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1762181014879-MigrationName.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/Services/EnterpriseLicenseService.js +9 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -0
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +285 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -0
- package/build/dist/UI/Components/Footer/Footer.js +6 -0
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Link/Link.js +2 -1
- package/build/dist/UI/Components/Link/Link.js.map +1 -1
- package/build/dist/UI/Config.js +1 -0
- package/build/dist/UI/Config.js.map +1 -1
- 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 "Talk to
|
|
534
|
+
Sales" 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
|
|
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:
|
|
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:
|
|
25
|
+
let children: ReactNode;
|
|
26
26
|
|
|
27
|
-
if (
|
|
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;
|
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
|