@oneuptime/common 10.0.35 → 10.0.36
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/Server/Infrastructure/Postgres/SchemaMigrations/{1773761409952-MigrationName.ts → 1774000000001-MigrationName.ts} +2 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
- package/Server/Types/Markdown.ts +11 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +4 -1
- package/Types/Code/CodeType.ts +1 -1
- package/Types/Metrics/MetricQueryConfigData.ts +1 -0
- package/Types/Monitor/CriteriaFilter.ts +19 -0
- package/Types/Monitor/KubernetesAlertTemplates.ts +703 -0
- package/Types/Monitor/KubernetesMetricCatalog.ts +347 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +86 -0
- package/Types/Monitor/MonitorStep.ts +36 -1
- package/Types/Monitor/MonitorStepKubernetesMonitor.ts +50 -0
- package/Types/Monitor/MonitorType.ts +14 -10
- package/UI/Components/AlertBanner/AlertBanner.tsx +69 -0
- package/UI/Components/ConditionsTable/ConditionsTable.tsx +149 -0
- package/UI/Components/Dictionary/DictionaryOfStingsViewer.tsx +35 -15
- package/UI/Components/ExpandableText/ExpandableText.tsx +42 -0
- package/UI/Components/FilterButtons/FilterButtons.tsx +60 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +4 -1
- package/UI/Components/ResourceUsageBar/ResourceUsageBar.tsx +58 -0
- package/UI/Components/StackedProgressBar/StackedProgressBar.tsx +81 -0
- package/UI/Components/StatusBadge/StatusBadge.tsx +44 -0
- package/UI/Components/Tabs/Tabs.tsx +36 -8
- package/UI/Utils/Dropdown.ts +2 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1773761409952-MigrationName.js → 1774000000001-MigrationName.js} +3 -3
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1773761409952-MigrationName.js.map → 1774000000001-MigrationName.js.map} +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
- package/build/dist/Server/Types/Markdown.js +10 -2
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +2 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Types/Code/CodeType.js +1 -1
- package/build/dist/Types/Code/CodeType.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +18 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +594 -0
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +311 -0
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +78 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +24 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepKubernetesMonitor.js +30 -0
- package/build/dist/Types/Monitor/MonitorStepKubernetesMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +13 -10
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/UI/Components/AlertBanner/AlertBanner.js +42 -0
- package/build/dist/UI/Components/AlertBanner/AlertBanner.js.map +1 -0
- package/build/dist/UI/Components/ConditionsTable/ConditionsTable.js +83 -0
- package/build/dist/UI/Components/ConditionsTable/ConditionsTable.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/DictionaryOfStingsViewer.js +14 -8
- package/build/dist/UI/Components/Dictionary/DictionaryOfStingsViewer.js.map +1 -1
- package/build/dist/UI/Components/ExpandableText/ExpandableText.js +19 -0
- package/build/dist/UI/Components/ExpandableText/ExpandableText.js.map +1 -0
- package/build/dist/UI/Components/FilterButtons/FilterButtons.js +17 -0
- package/build/dist/UI/Components/FilterButtons/FilterButtons.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +3 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/ResourceUsageBar/ResourceUsageBar.js +23 -0
- package/build/dist/UI/Components/ResourceUsageBar/ResourceUsageBar.js.map +1 -0
- package/build/dist/UI/Components/StackedProgressBar/StackedProgressBar.js +34 -0
- package/build/dist/UI/Components/StackedProgressBar/StackedProgressBar.js.map +1 -0
- package/build/dist/UI/Components/StatusBadge/StatusBadge.js +22 -0
- package/build/dist/UI/Components/StatusBadge/StatusBadge.js.map +1 -0
- package/build/dist/UI/Components/Tabs/Tabs.js +32 -9
- package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
- package/build/dist/UI/Utils/Dropdown.js +2 -1
- package/build/dist/UI/Utils/Dropdown.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export enum AlertBannerType {
|
|
4
|
+
Success = "success",
|
|
5
|
+
Warning = "warning",
|
|
6
|
+
Danger = "danger",
|
|
7
|
+
Info = "info",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComponentProps {
|
|
11
|
+
title: string;
|
|
12
|
+
type: AlertBannerType;
|
|
13
|
+
children?: ReactElement | undefined;
|
|
14
|
+
rightElement?: ReactElement | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const bannerStyles: Record<
|
|
19
|
+
AlertBannerType,
|
|
20
|
+
{ container: string; dot: string; title: string }
|
|
21
|
+
> = {
|
|
22
|
+
[AlertBannerType.Success]: {
|
|
23
|
+
container: "bg-emerald-50 border-emerald-200",
|
|
24
|
+
dot: "bg-emerald-500",
|
|
25
|
+
title: "text-emerald-800",
|
|
26
|
+
},
|
|
27
|
+
[AlertBannerType.Warning]: {
|
|
28
|
+
container: "bg-amber-50 border-amber-200",
|
|
29
|
+
dot: "bg-amber-500",
|
|
30
|
+
title: "text-amber-800",
|
|
31
|
+
},
|
|
32
|
+
[AlertBannerType.Danger]: {
|
|
33
|
+
container: "bg-red-50 border-red-200",
|
|
34
|
+
dot: "bg-red-500",
|
|
35
|
+
title: "text-red-800",
|
|
36
|
+
},
|
|
37
|
+
[AlertBannerType.Info]: {
|
|
38
|
+
container: "bg-blue-50 border-blue-200",
|
|
39
|
+
dot: "bg-blue-500",
|
|
40
|
+
title: "text-blue-800",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const AlertBanner: FunctionComponent<ComponentProps> = (
|
|
45
|
+
props: ComponentProps,
|
|
46
|
+
): ReactElement => {
|
|
47
|
+
const styles: { container: string; dot: string; title: string } =
|
|
48
|
+
bannerStyles[props.type];
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`rounded-lg border p-4 ${styles.container} ${props.className || ""}`}
|
|
53
|
+
role="alert"
|
|
54
|
+
>
|
|
55
|
+
<div className="flex items-center justify-between">
|
|
56
|
+
<div className="flex items-center gap-3">
|
|
57
|
+
<span className={`inline-flex h-3 w-3 rounded-full ${styles.dot}`} />
|
|
58
|
+
<span className={`text-lg font-semibold ${styles.title}`}>
|
|
59
|
+
{props.title}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
{props.rightElement && <div>{props.rightElement}</div>}
|
|
63
|
+
</div>
|
|
64
|
+
{props.children && <div className="mt-2">{props.children}</div>}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default AlertBanner;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import ExpandableText from "../ExpandableText/ExpandableText";
|
|
3
|
+
|
|
4
|
+
export interface Condition {
|
|
5
|
+
type: string;
|
|
6
|
+
status: string;
|
|
7
|
+
reason?: string | undefined;
|
|
8
|
+
message?: string | undefined;
|
|
9
|
+
lastTransitionTime?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ComponentProps {
|
|
13
|
+
conditions: Array<Condition>;
|
|
14
|
+
negativeTypes?: Array<string> | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Default condition types where "True" is bad
|
|
19
|
+
const defaultNegativeTypes: Array<string> = [
|
|
20
|
+
"MemoryPressure",
|
|
21
|
+
"DiskPressure",
|
|
22
|
+
"PIDPressure",
|
|
23
|
+
"NetworkUnavailable",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function isConditionBad(
|
|
27
|
+
condition: Condition,
|
|
28
|
+
negativeTypes: Array<string>,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (negativeTypes.includes(condition.type)) {
|
|
31
|
+
return condition.status === "True";
|
|
32
|
+
}
|
|
33
|
+
return condition.status === "False";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getStatusStyle(
|
|
37
|
+
condition: Condition,
|
|
38
|
+
negativeTypes: Array<string>,
|
|
39
|
+
): string {
|
|
40
|
+
const isNegativeType: boolean = negativeTypes.includes(condition.type);
|
|
41
|
+
if (condition.status === "True") {
|
|
42
|
+
return isNegativeType
|
|
43
|
+
? "bg-gradient-to-r from-red-50 to-red-100 text-red-800 ring-1 ring-inset ring-red-200/80"
|
|
44
|
+
: "bg-gradient-to-r from-emerald-50 to-emerald-100 text-emerald-800 ring-1 ring-inset ring-emerald-200/80";
|
|
45
|
+
}
|
|
46
|
+
if (condition.status === "False") {
|
|
47
|
+
return isNegativeType
|
|
48
|
+
? "bg-gradient-to-r from-emerald-50 to-emerald-100 text-emerald-800 ring-1 ring-inset ring-emerald-200/80"
|
|
49
|
+
: "bg-gradient-to-r from-red-50 to-red-100 text-red-800 ring-1 ring-inset ring-red-200/80";
|
|
50
|
+
}
|
|
51
|
+
return "bg-gradient-to-r from-amber-50 to-amber-100 text-amber-800 ring-1 ring-inset ring-amber-200/80";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatRelativeTime(timestamp: string): string {
|
|
55
|
+
if (!timestamp) {
|
|
56
|
+
return "-";
|
|
57
|
+
}
|
|
58
|
+
const date: Date = new Date(timestamp);
|
|
59
|
+
const now: Date = new Date();
|
|
60
|
+
const diffMs: number = now.getTime() - date.getTime();
|
|
61
|
+
if (diffMs < 0) {
|
|
62
|
+
return timestamp;
|
|
63
|
+
}
|
|
64
|
+
const diffSec: number = Math.floor(diffMs / 1000);
|
|
65
|
+
if (diffSec < 60) {
|
|
66
|
+
return `${diffSec}s ago`;
|
|
67
|
+
}
|
|
68
|
+
const diffMin: number = Math.floor(diffSec / 60);
|
|
69
|
+
if (diffMin < 60) {
|
|
70
|
+
return `${diffMin}m ago`;
|
|
71
|
+
}
|
|
72
|
+
const diffHrs: number = Math.floor(diffMin / 60);
|
|
73
|
+
if (diffHrs < 24) {
|
|
74
|
+
return `${diffHrs}h ago`;
|
|
75
|
+
}
|
|
76
|
+
const diffDays: number = Math.floor(diffHrs / 24);
|
|
77
|
+
return `${diffDays}d ago`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ConditionsTable: FunctionComponent<ComponentProps> = (
|
|
81
|
+
props: ComponentProps,
|
|
82
|
+
): ReactElement => {
|
|
83
|
+
const negativeTypes: Array<string> =
|
|
84
|
+
props.negativeTypes || defaultNegativeTypes;
|
|
85
|
+
|
|
86
|
+
if (props.conditions.length === 0) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="text-gray-500 text-sm p-4">No conditions available.</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={`overflow-x-auto ${props.className || ""}`}>
|
|
94
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
95
|
+
<thead className="bg-gray-50">
|
|
96
|
+
<tr>
|
|
97
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
98
|
+
Type
|
|
99
|
+
</th>
|
|
100
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
101
|
+
Status
|
|
102
|
+
</th>
|
|
103
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
104
|
+
Reason
|
|
105
|
+
</th>
|
|
106
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
107
|
+
Message
|
|
108
|
+
</th>
|
|
109
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
110
|
+
Last Transition
|
|
111
|
+
</th>
|
|
112
|
+
</tr>
|
|
113
|
+
</thead>
|
|
114
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
115
|
+
{props.conditions.map((condition: Condition, index: number) => {
|
|
116
|
+
const isBad: boolean = isConditionBad(condition, negativeTypes);
|
|
117
|
+
return (
|
|
118
|
+
<tr key={index} className={isBad ? "bg-red-50/50" : ""}>
|
|
119
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
120
|
+
{condition.type}
|
|
121
|
+
</td>
|
|
122
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
123
|
+
<span
|
|
124
|
+
className={`inline-flex px-2 py-0.5 text-xs font-semibold rounded-full ${getStatusStyle(condition, negativeTypes)}`}
|
|
125
|
+
>
|
|
126
|
+
{condition.status}
|
|
127
|
+
</span>
|
|
128
|
+
</td>
|
|
129
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
|
|
130
|
+
{condition.reason || "-"}
|
|
131
|
+
</td>
|
|
132
|
+
<td className="px-4 py-3 text-sm max-w-md">
|
|
133
|
+
<ExpandableText text={condition.message || "-"} />
|
|
134
|
+
</td>
|
|
135
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
136
|
+
<span title={condition.lastTransitionTime || ""}>
|
|
137
|
+
{formatRelativeTime(condition.lastTransitionTime || "")}
|
|
138
|
+
</span>
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</tbody>
|
|
144
|
+
</table>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default ConditionsTable;
|
|
@@ -40,22 +40,42 @@ const DictionaryOfStringsViewer: FunctionComponent<ComponentProps> = (
|
|
|
40
40
|
);
|
|
41
41
|
}, [props.value]);
|
|
42
42
|
|
|
43
|
+
if (data.length === 0) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="text-gray-400 text-sm py-2">No items to display.</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
return (
|
|
44
|
-
<div>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
<div className="overflow-x-auto">
|
|
51
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
52
|
+
<thead className="bg-gray-50">
|
|
53
|
+
<tr>
|
|
54
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
55
|
+
Key
|
|
56
|
+
</th>
|
|
57
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
58
|
+
Value
|
|
59
|
+
</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody className="bg-white divide-y divide-gray-100">
|
|
63
|
+
{data.map((item: Item, index: number) => {
|
|
64
|
+
return (
|
|
65
|
+
<tr key={index} className="hover:bg-gray-50/50">
|
|
66
|
+
<td className="px-4 py-2 text-sm font-mono font-medium text-indigo-700 whitespace-nowrap">
|
|
67
|
+
{item.key}
|
|
68
|
+
</td>
|
|
69
|
+
<td className="px-4 py-2 text-sm font-mono text-gray-600 break-all">
|
|
70
|
+
{item.value || (
|
|
71
|
+
<span className="text-gray-400 italic">empty</span>
|
|
72
|
+
)}
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
59
79
|
</div>
|
|
60
80
|
);
|
|
61
81
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ComponentProps {
|
|
4
|
+
text: string;
|
|
5
|
+
maxLength?: number | undefined;
|
|
6
|
+
className?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ExpandableText: FunctionComponent<ComponentProps> = (
|
|
10
|
+
props: ComponentProps,
|
|
11
|
+
): ReactElement => {
|
|
12
|
+
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
|
13
|
+
const maxLength: number = props.maxLength || 80;
|
|
14
|
+
|
|
15
|
+
if (!props.text || props.text === "-") {
|
|
16
|
+
return <span className="text-gray-400">-</span>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isLong: boolean = props.text.length > maxLength;
|
|
20
|
+
|
|
21
|
+
if (!isLong) {
|
|
22
|
+
return (
|
|
23
|
+
<span className={props.className || "text-gray-600"}>{props.text}</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span className={props.className || "text-gray-600"}>
|
|
29
|
+
{isExpanded ? props.text : props.text.substring(0, maxLength) + "..."}
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => {
|
|
32
|
+
setIsExpanded(!isExpanded);
|
|
33
|
+
}}
|
|
34
|
+
className="ml-1.5 text-xs text-indigo-600 hover:text-indigo-800 font-medium"
|
|
35
|
+
>
|
|
36
|
+
{isExpanded ? "Less" : "More"}
|
|
37
|
+
</button>
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default ExpandableText;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FilterButtonOption {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
badge?: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ComponentProps {
|
|
10
|
+
options: Array<FilterButtonOption>;
|
|
11
|
+
selectedValue: string;
|
|
12
|
+
onSelect: (value: string) => void;
|
|
13
|
+
className?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FilterButtons: FunctionComponent<ComponentProps> = (
|
|
17
|
+
props: ComponentProps,
|
|
18
|
+
): ReactElement => {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`inline-flex gap-1 ${props.className || ""}`}
|
|
22
|
+
role="radiogroup"
|
|
23
|
+
aria-label="Filter options"
|
|
24
|
+
>
|
|
25
|
+
{props.options.map((option: FilterButtonOption) => {
|
|
26
|
+
const isActive: boolean = props.selectedValue === option.value;
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
key={option.value}
|
|
30
|
+
onClick={() => {
|
|
31
|
+
props.onSelect(option.value);
|
|
32
|
+
}}
|
|
33
|
+
className={`px-3 py-1.5 text-xs rounded-md font-medium transition-all duration-150 ${
|
|
34
|
+
isActive
|
|
35
|
+
? "bg-indigo-100 text-indigo-800 ring-1 ring-inset ring-indigo-200"
|
|
36
|
+
: "bg-white text-gray-600 ring-1 ring-inset ring-gray-200 hover:bg-gray-50 hover:text-gray-800"
|
|
37
|
+
}`}
|
|
38
|
+
role="radio"
|
|
39
|
+
aria-checked={isActive}
|
|
40
|
+
>
|
|
41
|
+
{option.label}
|
|
42
|
+
{option.badge !== undefined && option.badge > 0 && (
|
|
43
|
+
<span
|
|
44
|
+
className={`ml-1.5 inline-flex min-w-[1.25rem] justify-center px-1 py-0 text-[10px] rounded-full ${
|
|
45
|
+
isActive
|
|
46
|
+
? "bg-indigo-200 text-indigo-900"
|
|
47
|
+
: "bg-gray-100 text-gray-500"
|
|
48
|
+
}`}
|
|
49
|
+
>
|
|
50
|
+
{option.badge}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default FilterButtons;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Icon from "../Icon/Icon";
|
|
2
2
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
3
3
|
import TinyFormDocumentation from "../TinyFormDocumentation/TinyFormDocumentation";
|
|
4
|
+
import DOMPurify from "dompurify";
|
|
4
5
|
import React, {
|
|
5
6
|
FunctionComponent,
|
|
6
7
|
ReactElement,
|
|
@@ -416,9 +417,11 @@ const MarkdownEditor: FunctionComponent<ComponentProps> = (
|
|
|
416
417
|
htmlContent = `<p class="mb-4">${htmlContent}</p>`;
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
const sanitizedContent: string = DOMPurify.sanitize(htmlContent);
|
|
421
|
+
|
|
419
422
|
return (
|
|
420
423
|
<div className="p-4 min-h-32 bg-white prose prose-sm max-w-none">
|
|
421
|
-
<div dangerouslySetInnerHTML={{ __html:
|
|
424
|
+
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
|
|
422
425
|
</div>
|
|
423
426
|
);
|
|
424
427
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ComponentProps {
|
|
4
|
+
label: string;
|
|
5
|
+
value: number; // percentage 0-100
|
|
6
|
+
valueLabel?: string | undefined;
|
|
7
|
+
secondaryLabel?: string | undefined;
|
|
8
|
+
heightClassName?: string | undefined;
|
|
9
|
+
className?: string | undefined;
|
|
10
|
+
labelWidthClassName?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getBarColor(percent: number): string {
|
|
14
|
+
if (percent > 80) {
|
|
15
|
+
return "bg-red-500";
|
|
16
|
+
}
|
|
17
|
+
if (percent > 60) {
|
|
18
|
+
return "bg-amber-500";
|
|
19
|
+
}
|
|
20
|
+
return "bg-emerald-500";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ResourceUsageBar: FunctionComponent<ComponentProps> = (
|
|
24
|
+
props: ComponentProps,
|
|
25
|
+
): ReactElement => {
|
|
26
|
+
const percent: number = Math.min(Math.max(props.value, 0), 100);
|
|
27
|
+
const heightClass: string = props.heightClassName || "h-2";
|
|
28
|
+
const labelWidthClass: string = props.labelWidthClassName || "w-40";
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`flex items-center gap-3 ${props.className || ""}`}>
|
|
32
|
+
<div
|
|
33
|
+
className={`${labelWidthClass} truncate text-sm text-gray-800 font-medium`}
|
|
34
|
+
title={props.label}
|
|
35
|
+
>
|
|
36
|
+
{props.label}
|
|
37
|
+
</div>
|
|
38
|
+
{props.secondaryLabel && (
|
|
39
|
+
<span className="inline-flex px-1.5 py-0.5 text-xs rounded bg-blue-50 text-blue-700">
|
|
40
|
+
{props.secondaryLabel}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
<div className={`flex-1 bg-gray-100 rounded-full ${heightClass}`}>
|
|
44
|
+
<div
|
|
45
|
+
className={`${heightClass} rounded-full transition-all duration-300 ${getBarColor(percent)}`}
|
|
46
|
+
style={{ width: `${percent}%` }}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
{props.valueLabel && (
|
|
50
|
+
<span className="text-xs text-gray-600 w-16 text-right font-medium tabular-nums">
|
|
51
|
+
{props.valueLabel}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default ResourceUsageBar;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface StackedProgressBarSegment {
|
|
4
|
+
value: number;
|
|
5
|
+
color: string; // Tailwind bg class, e.g. "bg-green-500"
|
|
6
|
+
label: string;
|
|
7
|
+
tooltip?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComponentProps {
|
|
11
|
+
segments: Array<StackedProgressBarSegment>;
|
|
12
|
+
totalValue?: number | undefined;
|
|
13
|
+
heightClassName?: string | undefined;
|
|
14
|
+
showLegend?: boolean | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const StackedProgressBar: FunctionComponent<ComponentProps> = (
|
|
19
|
+
props: ComponentProps,
|
|
20
|
+
): ReactElement => {
|
|
21
|
+
const total: number =
|
|
22
|
+
props.totalValue ||
|
|
23
|
+
props.segments.reduce((sum: number, seg: StackedProgressBarSegment) => {
|
|
24
|
+
return sum + seg.value;
|
|
25
|
+
}, 0);
|
|
26
|
+
|
|
27
|
+
const heightClass: string = props.heightClassName || "h-4";
|
|
28
|
+
const showLegend: boolean = props.showLegend !== false;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={props.className || ""}>
|
|
32
|
+
<div
|
|
33
|
+
className={`flex ${heightClass} rounded-full overflow-hidden bg-gray-100`}
|
|
34
|
+
role="progressbar"
|
|
35
|
+
aria-label="Stacked progress bar"
|
|
36
|
+
>
|
|
37
|
+
{props.segments.map(
|
|
38
|
+
(segment: StackedProgressBarSegment, index: number) => {
|
|
39
|
+
if (segment.value <= 0 || total <= 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const widthPercent: number = (segment.value / total) * 100;
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
key={index}
|
|
46
|
+
className={`${segment.color} ${heightClass} transition-all duration-300`}
|
|
47
|
+
style={{ width: `${widthPercent}%` }}
|
|
48
|
+
title={segment.tooltip || `${segment.label}: ${segment.value}`}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
{showLegend && (
|
|
55
|
+
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-2.5">
|
|
56
|
+
{props.segments
|
|
57
|
+
.filter((seg: StackedProgressBarSegment) => {
|
|
58
|
+
return seg.value > 0;
|
|
59
|
+
})
|
|
60
|
+
.map((segment: StackedProgressBarSegment, index: number) => {
|
|
61
|
+
return (
|
|
62
|
+
<div key={index} className="flex items-center gap-1.5">
|
|
63
|
+
<span
|
|
64
|
+
className={`inline-block w-2.5 h-2.5 rounded-full ${segment.color}`}
|
|
65
|
+
/>
|
|
66
|
+
<span className="text-sm text-gray-600">
|
|
67
|
+
{segment.label}{" "}
|
|
68
|
+
<span className="font-medium text-gray-800">
|
|
69
|
+
({segment.value})
|
|
70
|
+
</span>
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default StackedProgressBar;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export enum StatusBadgeType {
|
|
4
|
+
Success = "success",
|
|
5
|
+
Warning = "warning",
|
|
6
|
+
Danger = "danger",
|
|
7
|
+
Info = "info",
|
|
8
|
+
Neutral = "neutral",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ComponentProps {
|
|
12
|
+
text: string;
|
|
13
|
+
type?: StatusBadgeType | undefined;
|
|
14
|
+
className?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const statusStyles: Record<StatusBadgeType, string> = {
|
|
18
|
+
[StatusBadgeType.Success]:
|
|
19
|
+
"bg-gradient-to-r from-emerald-50 to-emerald-100 text-emerald-800 ring-1 ring-inset ring-emerald-200/80",
|
|
20
|
+
[StatusBadgeType.Warning]:
|
|
21
|
+
"bg-gradient-to-r from-amber-50 to-amber-100 text-amber-800 ring-1 ring-inset ring-amber-200/80",
|
|
22
|
+
[StatusBadgeType.Danger]:
|
|
23
|
+
"bg-gradient-to-r from-red-50 to-red-100 text-red-800 ring-1 ring-inset ring-red-200/80",
|
|
24
|
+
[StatusBadgeType.Info]:
|
|
25
|
+
"bg-gradient-to-r from-blue-50 to-blue-100 text-blue-800 ring-1 ring-inset ring-blue-200/80",
|
|
26
|
+
[StatusBadgeType.Neutral]:
|
|
27
|
+
"bg-gradient-to-r from-gray-50 to-gray-100 text-gray-700 ring-1 ring-inset ring-gray-200/80",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const StatusBadge: FunctionComponent<ComponentProps> = (
|
|
31
|
+
props: ComponentProps,
|
|
32
|
+
): ReactElement => {
|
|
33
|
+
const type: StatusBadgeType = props.type || StatusBadgeType.Neutral;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<span
|
|
37
|
+
className={`inline-flex items-center px-2.5 py-0.5 text-xs font-semibold rounded-full ${statusStyles[type]} ${props.className || ""}`}
|
|
38
|
+
>
|
|
39
|
+
{props.text}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default StatusBadge;
|
|
@@ -3,6 +3,7 @@ import React, {
|
|
|
3
3
|
FunctionComponent,
|
|
4
4
|
ReactElement,
|
|
5
5
|
useEffect,
|
|
6
|
+
useRef,
|
|
6
7
|
useState,
|
|
7
8
|
} from "react";
|
|
8
9
|
|
|
@@ -14,19 +15,46 @@ export interface ComponentProps {
|
|
|
14
15
|
const Tabs: FunctionComponent<ComponentProps> = (
|
|
15
16
|
props: ComponentProps,
|
|
16
17
|
): ReactElement => {
|
|
17
|
-
const [
|
|
18
|
+
const [currentTabName, setCurrentTabName] = useState<string | null>(null);
|
|
19
|
+
const hasInitialized: React.MutableRefObject<boolean> =
|
|
20
|
+
useRef<boolean>(false);
|
|
18
21
|
|
|
22
|
+
// Initialize current tab only once, or when the tab list names change
|
|
19
23
|
useEffect(() => {
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const tabNames: Array<string> = props.tabs.map((t: Tab) => {
|
|
25
|
+
return t.name;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!hasInitialized.current && props.tabs.length > 0) {
|
|
29
|
+
hasInitialized.current = true;
|
|
30
|
+
setCurrentTabName(props.tabs[0]!.name);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If current tab no longer exists in the list, reset to first
|
|
35
|
+
if (currentTabName && !tabNames.includes(currentTabName)) {
|
|
36
|
+
setCurrentTabName(props.tabs.length > 0 ? props.tabs[0]!.name : null);
|
|
37
|
+
}
|
|
38
|
+
}, [
|
|
39
|
+
props.tabs
|
|
40
|
+
.map((t: Tab) => {
|
|
41
|
+
return t.name;
|
|
42
|
+
})
|
|
43
|
+
.join(","),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Find the current tab object by name
|
|
47
|
+
const currentTab: Tab | undefined = props.tabs.find((t: Tab) => {
|
|
48
|
+
return t.name === currentTabName;
|
|
49
|
+
});
|
|
22
50
|
|
|
23
51
|
useEffect(() => {
|
|
24
52
|
if (currentTab && props.onTabChange) {
|
|
25
53
|
props.onTabChange(currentTab);
|
|
26
54
|
}
|
|
27
|
-
}, [
|
|
55
|
+
}, [currentTabName]);
|
|
28
56
|
|
|
29
|
-
const tabPanelId: string = `tabpanel-${
|
|
57
|
+
const tabPanelId: string = `tabpanel-${currentTabName || "default"}`;
|
|
30
58
|
|
|
31
59
|
return (
|
|
32
60
|
<div>
|
|
@@ -41,9 +69,9 @@ const Tabs: FunctionComponent<ComponentProps> = (
|
|
|
41
69
|
key={tab.name}
|
|
42
70
|
tab={tab}
|
|
43
71
|
onClick={() => {
|
|
44
|
-
|
|
72
|
+
setCurrentTabName(tab.name);
|
|
45
73
|
}}
|
|
46
|
-
isSelected={tab ===
|
|
74
|
+
isSelected={tab.name === currentTabName}
|
|
47
75
|
tabPanelId={tabPanelId}
|
|
48
76
|
/>
|
|
49
77
|
);
|
|
@@ -52,7 +80,7 @@ const Tabs: FunctionComponent<ComponentProps> = (
|
|
|
52
80
|
<div
|
|
53
81
|
id={tabPanelId}
|
|
54
82
|
role="tabpanel"
|
|
55
|
-
aria-labelledby={`tab-${
|
|
83
|
+
aria-labelledby={`tab-${currentTabName || "default"}`}
|
|
56
84
|
className="mt-3 ml-1"
|
|
57
85
|
>
|
|
58
86
|
{currentTab && currentTab.children}
|
package/UI/Utils/Dropdown.ts
CHANGED
|
@@ -80,7 +80,8 @@ export default class DropdownUtil {
|
|
|
80
80
|
public static getDropdownOptionsFromArray(
|
|
81
81
|
arr: Array<string>,
|
|
82
82
|
): Array<DropdownOption> {
|
|
83
|
-
|
|
83
|
+
const uniqueArr: Array<string> = [...new Set(arr)];
|
|
84
|
+
return uniqueArr.map((item: string) => {
|
|
84
85
|
return {
|
|
85
86
|
label: item,
|
|
86
87
|
value: item,
|