@oneuptime/common 10.4.14 → 10.4.15

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 (152) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +49 -0
  2. package/Models/AnalyticsModels/AuditLog.ts +8 -0
  3. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  4. package/Models/AnalyticsModels/Log.ts +1 -0
  5. package/Models/AnalyticsModels/Metric.ts +10 -0
  6. package/Models/AnalyticsModels/MonitorLog.ts +1 -0
  7. package/Models/AnalyticsModels/Profile.ts +1 -0
  8. package/Models/AnalyticsModels/ProfileSample.ts +1 -0
  9. package/Models/AnalyticsModels/Span.ts +1 -0
  10. package/Models/DatabaseModels/AlertCustomField.ts +37 -0
  11. package/Models/DatabaseModels/IncidentCustomField.ts +37 -0
  12. package/Models/DatabaseModels/IncidentMember.ts +9 -0
  13. package/Models/DatabaseModels/MonitorCustomField.ts +37 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +37 -0
  15. package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +37 -0
  16. package/Models/DatabaseModels/StatusPageCustomField.ts +37 -0
  17. package/Models/DatabaseModels/TableView.ts +40 -0
  18. package/Models/DatabaseModels/TeamMemberCustomField.ts +37 -0
  19. package/Server/API/BaseAnalyticsAPI.ts +128 -20
  20. package/Server/API/MetricAPI.ts +5 -138
  21. package/Server/API/StatusAPI.ts +103 -7
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.ts +13 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.ts +34 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.ts +67 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Services/AccessTokenService.ts +1 -1
  27. package/Server/Services/AnalyticsDatabaseService.ts +24 -4
  28. package/Server/Services/MetricService.ts +113 -0
  29. package/Server/Services/ProjectService.ts +21 -1
  30. package/Server/Utils/Response.ts +4 -1
  31. package/Server/Utils/UserPermission/UserPermission.ts +17 -1
  32. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +2 -2
  33. package/Types/API/HTTPResponse.ts +16 -0
  34. package/Types/BaseDatabase/ListResult.ts +6 -0
  35. package/Types/CustomField/CustomFieldType.ts +2 -0
  36. package/Types/Date.ts +9 -1
  37. package/Types/ListData.ts +14 -0
  38. package/Types/Monitor/DnsMonitor/DnsMonitorResponse.ts +3 -0
  39. package/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.ts +5 -0
  40. package/Types/Monitor/DomainMonitor/DomainMonitorResponse.ts +4 -0
  41. package/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.ts +4 -0
  42. package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +3 -0
  43. package/Types/Probe/ProbeAttempt.ts +9 -0
  44. package/Types/Probe/ProbeMonitorResponse.ts +3 -0
  45. package/UI/Components/BulkUpdate/BulkOwnerActions.tsx +504 -0
  46. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +64 -54
  47. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +38 -0
  48. package/UI/Components/CustomFields/DropdownOptionsInput.tsx +150 -0
  49. package/UI/Components/Detail/Detail.tsx +78 -11
  50. package/UI/Components/List/List.tsx +6 -0
  51. package/UI/Components/ModelTable/BaseModelTable.tsx +74 -2
  52. package/UI/Components/ModelTable/TableView.tsx +70 -30
  53. package/UI/Components/Pagination/Pagination.tsx +75 -33
  54. package/UI/Components/Table/Table.tsx +6 -0
  55. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +1 -0
  56. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +33 -0
  57. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/AuditLog.js +8 -0
  59. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  61. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  63. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/Metric.js +10 -0
  65. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  66. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  67. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  68. package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
  69. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  70. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -0
  71. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  72. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  73. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  74. package/build/dist/Models/DatabaseModels/AlertCustomField.js +38 -0
  75. package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +38 -0
  77. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/IncidentMember.js +11 -1
  79. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/MonitorCustomField.js +38 -0
  81. package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +38 -0
  83. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
  84. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +38 -0
  85. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +38 -0
  87. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  89. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +38 -0
  91. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
  92. package/build/dist/Server/API/BaseAnalyticsAPI.js +105 -18
  93. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  94. package/build/dist/Server/API/MetricAPI.js +5 -113
  95. package/build/dist/Server/API/MetricAPI.js.map +1 -1
  96. package/build/dist/Server/API/StatusAPI.js +75 -8
  97. package/build/dist/Server/API/StatusAPI.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js +12 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js.map +1 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js +27 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js.map +1 -0
  102. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js +28 -0
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js.map +1 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  106. package/build/dist/Server/Services/AccessTokenService.js +1 -1
  107. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  108. package/build/dist/Server/Services/AnalyticsDatabaseService.js +22 -3
  109. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  110. package/build/dist/Server/Services/MetricService.js +89 -0
  111. package/build/dist/Server/Services/MetricService.js.map +1 -1
  112. package/build/dist/Server/Services/ProjectService.js +19 -1
  113. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  114. package/build/dist/Server/Utils/Response.js +6 -5
  115. package/build/dist/Server/Utils/Response.js.map +1 -1
  116. package/build/dist/Server/Utils/UserPermission/UserPermission.js +13 -1
  117. package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
  118. package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js +2 -2
  119. package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js.map +1 -1
  120. package/build/dist/Types/API/HTTPResponse.js +15 -0
  121. package/build/dist/Types/API/HTTPResponse.js.map +1 -1
  122. package/build/dist/Types/CustomField/CustomFieldType.js +2 -0
  123. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  124. package/build/dist/Types/Date.js +10 -1
  125. package/build/dist/Types/Date.js.map +1 -1
  126. package/build/dist/Types/ListData.js +4 -0
  127. package/build/dist/Types/ListData.js.map +1 -1
  128. package/build/dist/Types/Probe/ProbeAttempt.js +2 -0
  129. package/build/dist/Types/Probe/ProbeAttempt.js.map +1 -0
  130. package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js +376 -0
  131. package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js.map +1 -0
  132. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +32 -25
  133. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  134. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +32 -0
  135. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  136. package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js +84 -0
  137. package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js.map +1 -0
  138. package/build/dist/UI/Components/Detail/Detail.js +34 -3
  139. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  140. package/build/dist/UI/Components/List/List.js +1 -1
  141. package/build/dist/UI/Components/List/List.js.map +1 -1
  142. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +45 -5
  143. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  144. package/build/dist/UI/Components/ModelTable/TableView.js +40 -19
  145. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  146. package/build/dist/UI/Components/Pagination/Pagination.js +62 -36
  147. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  148. package/build/dist/UI/Components/Table/Table.js +1 -1
  149. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  150. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +1 -0
  151. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  152. package/package.json +1 -1
@@ -4,11 +4,13 @@ import { ButtonStyleType } from "../Button/Button";
4
4
  import Card from "../Card/Card";
5
5
  import ComponentLoader from "../ComponentLoader/ComponentLoader";
6
6
  import Detail from "../Detail/Detail";
7
+ import { DropdownOption } from "../Dropdown/Dropdown";
7
8
  import ErrorMessage from "../ErrorMessage/ErrorMessage";
8
9
  import BasicFormModal from "../FormModal/BasicFormModal";
9
10
  import BaseModel, {
10
11
  DatabaseBaseModelType,
11
12
  } from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
13
+ import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
12
14
  import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
13
15
  import { PromiseVoidFunction } from "../../../Types/FunctionTypes";
14
16
  import IconProp from "../../../Types/Icon/IconProp";
@@ -17,6 +19,25 @@ import ObjectID from "../../../Types/ObjectID";
17
19
  import React, { FunctionComponent, ReactElement, useState } from "react";
18
20
  import useAsyncEffect from "use-async-effect";
19
21
 
22
+ const parseDropdownOptions: (value: unknown) => Array<DropdownOption> = (
23
+ value: unknown,
24
+ ): Array<DropdownOption> => {
25
+ if (typeof value !== "string" || !value) {
26
+ return [];
27
+ }
28
+ return value
29
+ .split("\n")
30
+ .map((line: string) => {
31
+ return line.trim();
32
+ })
33
+ .filter((line: string) => {
34
+ return line.length > 0;
35
+ })
36
+ .map((line: string) => {
37
+ return { label: line, value: line };
38
+ });
39
+ };
40
+
20
41
  export interface ComponentProps {
21
42
  title: string;
22
43
  description: string;
@@ -54,6 +75,7 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
54
75
  name: true,
55
76
  customFieldType: true,
56
77
  description: true,
78
+ dropdownOptions: true,
57
79
  } as any,
58
80
  sort: {},
59
81
  });
@@ -137,12 +159,20 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
137
159
  id={props.name}
138
160
  item={(model as any)["customFields"] || {}}
139
161
  fields={schemaList.map((schemaItem: BaseModel) => {
162
+ const isDropdown: boolean =
163
+ (schemaItem as any).customFieldType ===
164
+ CustomFieldType.Dropdown ||
165
+ (schemaItem as any).customFieldType ===
166
+ CustomFieldType.MultiSelectDropdown;
140
167
  return {
141
168
  key: (schemaItem as any).name,
142
169
  title: (schemaItem as any).name,
143
170
  description: (schemaItem as any).description,
144
171
  fieldType: (schemaItem as any).customFieldType,
145
172
  placeholder: "No data entered",
173
+ dropdownOptions: isDropdown
174
+ ? parseDropdownOptions((schemaItem as any).dropdownOptions)
175
+ : undefined,
146
176
  };
147
177
  })}
148
178
  showDetailsInNumberOfColumns={1}
@@ -161,6 +191,11 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
161
191
  formProps={{
162
192
  initialValues: (model as any)["customFields"] || {},
163
193
  fields: schemaList.map((schemaItem: BaseModel) => {
194
+ const isDropdown: boolean =
195
+ (schemaItem as any).customFieldType ===
196
+ CustomFieldType.Dropdown ||
197
+ (schemaItem as any).customFieldType ===
198
+ CustomFieldType.MultiSelectDropdown;
164
199
  return {
165
200
  field: {
166
201
  [(schemaItem as any).name]: true,
@@ -170,6 +205,9 @@ const CustomFieldsDetail: FunctionComponent<ComponentProps> = (
170
205
  fieldType: (schemaItem as any).customFieldType,
171
206
  required: false,
172
207
  placeholder: "",
208
+ dropdownOptions: isDropdown
209
+ ? parseDropdownOptions((schemaItem as any).dropdownOptions)
210
+ : undefined,
173
211
  };
174
212
  }),
175
213
  }}
@@ -0,0 +1,150 @@
1
+ import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
2
+ import Input, { InputType } from "../Input/Input";
3
+ import IconProp from "../../../Types/Icon/IconProp";
4
+ import React, {
5
+ FunctionComponent,
6
+ MutableRefObject,
7
+ ReactElement,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+
13
+ export interface ComponentProps {
14
+ initialValue?: string | undefined;
15
+ onChange?: ((value: string) => void) | undefined;
16
+ placeholder?: string | undefined;
17
+ error?: string | undefined;
18
+ onBlur?: (() => void) | undefined;
19
+ }
20
+
21
+ const parseValue: (value: string | undefined) => Array<string> = (
22
+ value: string | undefined,
23
+ ): Array<string> => {
24
+ if (!value) {
25
+ return [];
26
+ }
27
+ return value
28
+ .split("\n")
29
+ .map((line: string) => {
30
+ return line.trim();
31
+ })
32
+ .filter((line: string) => {
33
+ return line.length > 0;
34
+ });
35
+ };
36
+
37
+ const serializeValue: (options: Array<string>) => string = (
38
+ options: Array<string>,
39
+ ): string => {
40
+ return options
41
+ .map((option: string) => {
42
+ return option.trim();
43
+ })
44
+ .filter((option: string) => {
45
+ return option.length > 0;
46
+ })
47
+ .join("\n");
48
+ };
49
+
50
+ const DropdownOptionsInput: FunctionComponent<ComponentProps> = (
51
+ props: ComponentProps,
52
+ ): ReactElement => {
53
+ const [options, setOptions] = useState<Array<string>>(() => {
54
+ const parsed: Array<string> = parseValue(props.initialValue);
55
+ return parsed.length > 0 ? parsed : [""];
56
+ });
57
+
58
+ const lastEmittedRef: MutableRefObject<string> = useRef<string>(
59
+ serializeValue(parseValue(props.initialValue)),
60
+ );
61
+
62
+ useEffect(() => {
63
+ const serialized: string = serializeValue(options);
64
+ if (serialized !== lastEmittedRef.current) {
65
+ lastEmittedRef.current = serialized;
66
+ if (props.onChange) {
67
+ props.onChange(serialized);
68
+ }
69
+ }
70
+ }, [options]);
71
+
72
+ type UpdateAtFn = (index: number, value: string) => void;
73
+ const updateAt: UpdateAtFn = (index: number, value: string): void => {
74
+ setOptions((prev: Array<string>) => {
75
+ const next: Array<string> = [...prev];
76
+ next[index] = value;
77
+ return next;
78
+ });
79
+ };
80
+
81
+ type RemoveAtFn = (index: number) => void;
82
+ const removeAt: RemoveAtFn = (index: number): void => {
83
+ setOptions((prev: Array<string>) => {
84
+ const next: Array<string> = prev.filter((_: string, i: number) => {
85
+ return i !== index;
86
+ });
87
+ return next.length > 0 ? next : [""];
88
+ });
89
+ };
90
+
91
+ type AddOptionFn = () => void;
92
+ const addOption: AddOptionFn = (): void => {
93
+ setOptions((prev: Array<string>) => {
94
+ return [...prev, ""];
95
+ });
96
+ };
97
+
98
+ return (
99
+ <div>
100
+ <div className="space-y-2">
101
+ {options.map((value: string, index: number) => {
102
+ return (
103
+ <div key={index} className="flex items-center gap-2">
104
+ <div className="flex h-7 w-7 items-center justify-center rounded-md bg-gray-100 text-xs font-medium text-gray-500">
105
+ {index + 1}
106
+ </div>
107
+ <div className="flex-1">
108
+ <Input
109
+ value={value}
110
+ placeholder={props.placeholder || `Option ${index + 1}`}
111
+ onChange={(newValue: string) => {
112
+ updateAt(index, newValue);
113
+ }}
114
+ onBlur={() => {
115
+ if (props.onBlur) {
116
+ props.onBlur();
117
+ }
118
+ }}
119
+ type={InputType.TEXT}
120
+ />
121
+ </div>
122
+ <Button
123
+ title="Remove"
124
+ buttonStyle={ButtonStyleType.ICON}
125
+ icon={IconProp.Trash}
126
+ onClick={() => {
127
+ removeAt(index);
128
+ }}
129
+ />
130
+ </div>
131
+ );
132
+ })}
133
+ </div>
134
+ <div className="mt-3">
135
+ <Button
136
+ title="Add Option"
137
+ icon={IconProp.Add}
138
+ buttonSize={ButtonSize.Small}
139
+ buttonStyle={ButtonStyleType.NORMAL}
140
+ onClick={addOption}
141
+ />
142
+ </div>
143
+ {props.error ? (
144
+ <p className="mt-2 text-sm text-red-500">{props.error}</p>
145
+ ) : null}
146
+ </div>
147
+ );
148
+ };
149
+
150
+ export default DropdownOptionsInput;
@@ -113,25 +113,84 @@ const Detail: DetailFunction = <T extends GenericObject>(
113
113
  }
114
114
 
115
115
  return (
116
- <span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-indigo-50 border border-indigo-100 text-indigo-700 text-sm font-medium">
116
+ <span className="inline-flex items-center gap-x-1.5 px-2 py-1 rounded-md bg-indigo-50 text-indigo-700 text-xs font-medium ring-1 ring-inset ring-indigo-700/10">
117
117
  <svg
118
- className="w-3.5 h-3.5 text-indigo-500"
119
- fill="none"
120
- viewBox="0 0 24 24"
121
- stroke="currentColor"
118
+ className="h-1.5 w-1.5 fill-indigo-500"
119
+ viewBox="0 0 6 6"
120
+ aria-hidden="true"
122
121
  >
123
- <path
124
- strokeLinecap="round"
125
- strokeLinejoin="round"
126
- strokeWidth={2}
127
- d="M9 5l7 7-7 7"
128
- />
122
+ <circle cx={3} cy={3} r={3} />
129
123
  </svg>
130
124
  {selectedOption.label as string}
131
125
  </span>
132
126
  );
133
127
  };
134
128
 
129
+ type GetMultiSelectDropdownViewerFunction = (
130
+ data: Array<string | number | boolean> | string,
131
+ options: Array<DropdownOption | DropdownOptionGroup>,
132
+ placeholder: string,
133
+ ) => ReactElement;
134
+
135
+ const getMultiSelectDropdownViewer: GetMultiSelectDropdownViewerFunction = (
136
+ data: Array<string | number | boolean> | string,
137
+ options: Array<DropdownOption | DropdownOptionGroup>,
138
+ placeholder: string,
139
+ ): ReactElement => {
140
+ const values: Array<string | number | boolean> = Array.isArray(data)
141
+ ? data
142
+ : typeof data === "string" && data.length > 0
143
+ ? [data]
144
+ : [];
145
+
146
+ if (values.length === 0) {
147
+ return (
148
+ <span className="text-gray-400 italic text-sm">{placeholder}</span>
149
+ );
150
+ }
151
+
152
+ const flatOptions: Array<DropdownOption> = (options || []).flatMap(
153
+ (item: DropdownOption | DropdownOptionGroup) => {
154
+ if ("options" in item && Array.isArray(item.options)) {
155
+ return item.options;
156
+ }
157
+ return [item as DropdownOption];
158
+ },
159
+ );
160
+
161
+ return (
162
+ <div className="flex flex-wrap gap-1.5">
163
+ {values.map(
164
+ (value: string | number | boolean, index: number): ReactElement => {
165
+ const matched: DropdownOption | undefined = flatOptions.find(
166
+ (option: DropdownOption) => {
167
+ return option.value === value;
168
+ },
169
+ );
170
+ const label: string = matched
171
+ ? (matched.label as string)
172
+ : String(value);
173
+ return (
174
+ <span
175
+ key={`${String(value)}-${index}`}
176
+ className="inline-flex items-center gap-x-1.5 px-2 py-1 rounded-md bg-indigo-50 text-indigo-700 text-xs font-medium ring-1 ring-inset ring-indigo-700/10"
177
+ >
178
+ <svg
179
+ className="h-1.5 w-1.5 fill-indigo-500"
180
+ viewBox="0 0 6 6"
181
+ aria-hidden="true"
182
+ >
183
+ <circle cx={3} cy={3} r={3} />
184
+ </svg>
185
+ {label}
186
+ </span>
187
+ );
188
+ },
189
+ )}
190
+ </div>
191
+ );
192
+ };
193
+
135
194
  type GetDictionaryOfStringsViewerFunction = (
136
195
  data: Dictionary<string>,
137
196
  ) => ReactElement;
@@ -432,6 +491,14 @@ const Detail: DetailFunction = <T extends GenericObject>(
432
491
  );
433
492
  }
434
493
 
494
+ if (field.fieldType === FieldType.MultiSelectDropdown) {
495
+ data = getMultiSelectDropdownViewer(
496
+ data as Array<string | number | boolean> | string,
497
+ field.dropdownOptions || [],
498
+ field.placeholder as string,
499
+ );
500
+ }
501
+
435
502
  if (data && field.fieldType === FieldType.HiddenText) {
436
503
  data = (
437
504
  <HiddenText
@@ -21,6 +21,11 @@ export interface ComponentProps<T extends GenericObject> {
21
21
  onNavigateToPage: (pageNumber: number, itemsOnPage: number) => void;
22
22
  currentPageNumber: number;
23
23
  totalItemsCount: number;
24
+ /*
25
+ * Optional. Forwarded to Pagination. When set, count is a lower
26
+ * bound and pagination switches to a prev/next-only UI.
27
+ */
28
+ hasMore?: boolean | undefined;
24
29
  itemsOnPage: number;
25
30
  enableDragAndDrop?: boolean | undefined;
26
31
  dragDropIndexField?: keyof T | undefined;
@@ -141,6 +146,7 @@ const List: ListFunction = <T extends GenericObject>(
141
146
  pluralLabel={props.pluralLabel}
142
147
  currentPageNumber={props.currentPageNumber}
143
148
  totalItemsCount={props.totalItemsCount}
149
+ hasMore={props.hasMore}
144
150
  itemsOnPage={props.itemsOnPage}
145
151
  onNavigateToPage={props.onNavigateToPage}
146
152
  isLoading={props.isLoading}
@@ -161,6 +161,11 @@ export interface BaseTableProps<
161
161
  | undefined
162
162
  | ((data: Array<TBaseModel>, totalCount: number) => void);
163
163
  cardProps?: CardComponentProps | undefined;
164
+ /**
165
+ * Optional content rendered inside the card body, above the table rows.
166
+ * Useful for in-table filter chips, alerts, etc.
167
+ */
168
+ topContent?: ReactElement | undefined;
164
169
  helpContent?:
165
170
  | {
166
171
  title: string;
@@ -240,6 +245,19 @@ export interface BaseTableProps<
240
245
 
241
246
  saveFilterProps?: SaveFilterProps | undefined;
242
247
 
248
+ /**
249
+ * Extra serializable state to persist alongside the saved view (e.g. facet
250
+ * selections from a parent hook). Stored on `TableView.facets`. The shape
251
+ * is opaque to ModelTable.
252
+ */
253
+ currentFacetState?: JSONObject | undefined;
254
+ /**
255
+ * Called when a saved view is loaded so the parent can restore its facet
256
+ * state. `null` means "no saved facet data" (e.g. the user reset to the
257
+ * default empty view).
258
+ */
259
+ onFacetStateRestored?: ((state: JSONObject | null) => void) | undefined;
260
+
243
261
  onFilterApplied?: ((isFilterApplied: boolean) => void) | undefined;
244
262
 
245
263
  formSummary?: FormSummaryConfig | undefined;
@@ -363,6 +381,13 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
363
381
  const [query, setQuery] = useState<Query<TBaseModel>>({});
364
382
  const [currentPageNumber, setCurrentPageNumber] = useState<number>(1);
365
383
  const [totalItemsCount, setTotalItemsCount] = useState<number>(0);
384
+ /*
385
+ * Analytics endpoints (Log/Span/Metric/...) skip COUNT(*) for
386
+ * performance and instead return `hasMore`. When `hasMore` is
387
+ * `undefined`, the endpoint emitted an exact `count` and the
388
+ * pagination falls back to the count-based UI.
389
+ */
390
+ const [hasMore, setHasMore] = useState<boolean | undefined>(undefined);
366
391
  const [isLoading, setIsLoading] = useState<boolean>(true);
367
392
 
368
393
  const [error, setError] = useState<string>("");
@@ -1171,7 +1196,16 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1171
1196
  });
1172
1197
 
1173
1198
  setTotalItemsCount(listResult.count);
1199
+ setHasMore(listResult.hasMore);
1174
1200
  setData(listResult.data);
1201
+ /*
1202
+ * Fire onFetchSuccess so consumers (e.g. the resource-owners hook)
1203
+ * can react to the loaded page. Previously the prop was declared
1204
+ * but never invoked, which broke per-row owner enrichment.
1205
+ */
1206
+ if (props.onFetchSuccess) {
1207
+ props.onFetchSuccess(listResult.data, listResult.count);
1208
+ }
1175
1209
  } catch (err) {
1176
1210
  setError(API.getFriendlyMessage(err));
1177
1211
  }
@@ -1260,6 +1294,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1260
1294
  currentSortBy={sortBy}
1261
1295
  currentItemsOnPage={itemsOnPage}
1262
1296
  currentSortOrder={sortOrder}
1297
+ currentFacetState={props.currentFacetState}
1263
1298
  onViewChange={async (tableView: TableView | null) => {
1264
1299
  setTableView(tableView);
1265
1300
 
@@ -1286,12 +1321,22 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1286
1321
  if (classicTableFilters.length === 0) {
1287
1322
  await getFilterDropdownItems();
1288
1323
  }
1324
+
1325
+ if (props.onFacetStateRestored) {
1326
+ props.onFacetStateRestored(
1327
+ (tableView.facets as JSONObject | undefined) || null,
1328
+ );
1329
+ }
1289
1330
  } else {
1290
1331
  setQuery({});
1291
1332
  setSortBy(null);
1292
1333
  setSortOrder(SortOrder.Descending);
1293
1334
  setItemsOnPage(10);
1294
1335
  setCurrentPageNumber(1);
1336
+
1337
+ if (props.onFacetStateRestored) {
1338
+ props.onFacetStateRestored(null);
1339
+ }
1295
1340
  }
1296
1341
  }}
1297
1342
  />
@@ -1577,6 +1622,17 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1577
1622
  serializeToTableColumns();
1578
1623
  }, [data]);
1579
1624
 
1625
+ /*
1626
+ * Re-serialize whenever the parent passes a new `columns` reference. The
1627
+ * column array carries `getElement` closures that capture parent state
1628
+ * (e.g. an owners map populated asynchronously). Without this, the cell
1629
+ * renders are frozen at first paint and never see updated state — which
1630
+ * is what made the Owners column stick on "Loading…" forever.
1631
+ */
1632
+ useEffect(() => {
1633
+ serializeToTableColumns();
1634
+ }, [props.columns]);
1635
+
1580
1636
  const setActionSchema: VoidFunction = () => {
1581
1637
  const permissions: Array<Permission> = PermissionUtil.getAllPermissions();
1582
1638
 
@@ -2063,6 +2119,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2063
2119
  dragDropIdField={"_id"}
2064
2120
  dragDropIndexField={props.dragDropIndexField}
2065
2121
  totalItemsCount={totalItemsCount}
2122
+ hasMore={hasMore}
2066
2123
  data={data}
2067
2124
  id={props.id}
2068
2125
  columns={tableColumns}
@@ -2219,6 +2276,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2219
2276
  dragDropIndexField={props.dragDropIndexField}
2220
2277
  isLoading={getTableLoadingState()}
2221
2278
  totalItemsCount={totalItemsCount}
2279
+ hasMore={hasMore}
2222
2280
  data={data}
2223
2281
  id={props.id}
2224
2282
  fields={fields}
@@ -2429,7 +2487,8 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2429
2487
  })
2430
2488
  .filter((l: SearchLabelOption) => {
2431
2489
  return (
2432
- lowerPrefix.length === 0 || l.name.toLowerCase().includes(lowerPrefix)
2490
+ lowerPrefix.length === 0 ||
2491
+ l.name.toLowerCase().startsWith(lowerPrefix)
2433
2492
  );
2434
2493
  })
2435
2494
  .slice(0, 8);
@@ -2600,7 +2659,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2600
2659
  />
2601
2660
  </div>
2602
2661
  )}
2603
- {showMatchPill && totalItemsCount >= 0 && (
2662
+ {showMatchPill && totalItemsCount >= 0 && hasMore === undefined && (
2604
2663
  <span
2605
2664
  className="flex-none whitespace-nowrap rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700"
2606
2665
  title={`${totalItemsCount} ${totalItemsCount === 1 ? "result" : "results"}`}
@@ -2608,6 +2667,17 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2608
2667
  {totalItemsCount} {totalItemsCount === 1 ? "match" : "matches"}
2609
2668
  </span>
2610
2669
  )}
2670
+ {showMatchPill && hasMore !== undefined && data.length > 0 && (
2671
+ <span
2672
+ className="flex-none whitespace-nowrap rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700"
2673
+ title={`${data.length}${hasMore ? "+" : ""} ${
2674
+ data.length === 1 ? "result" : "results"
2675
+ } on this page`}
2676
+ >
2677
+ {data.length}
2678
+ {hasMore ? "+" : ""} {data.length === 1 ? "match" : "matches"}
2679
+ </span>
2680
+ )}
2611
2681
  {searchText.length > 0 || selectedLabels.length > 0 ? (
2612
2682
  <button
2613
2683
  type="button"
@@ -3055,6 +3125,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
3055
3125
  ) : (
3056
3126
  <></>
3057
3127
  )}
3128
+ {props.topContent || <></>}
3058
3129
  {tableColumns.length > 0 && showAs === ShowAs.Table ? (
3059
3130
  getTable()
3060
3131
  ) : (
@@ -3081,6 +3152,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
3081
3152
  ) : (
3082
3153
  <></>
3083
3154
  )}
3155
+ {!props.cardProps && props.topContent}
3084
3156
  {!props.cardProps && showAs === ShowAs.Table ? getTable() : <></>}
3085
3157
  {!props.cardProps && showAs === ShowAs.List ? getList() : <></>}
3086
3158
  </div>