@oneuptime/common 12.0.1 → 12.0.2
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/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -7,6 +7,7 @@ import Typeof from "../../../Types/Typeof";
|
|
|
7
7
|
import { FindOperator, Raw } from "typeorm";
|
|
8
8
|
import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
|
|
9
9
|
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
|
|
10
|
+
import buildJSONColumnQuery, { JSONColumnQuery } from "./JSONColumnQuery";
|
|
10
11
|
|
|
11
12
|
export default class QueryHelper {
|
|
12
13
|
@CaptureSpan()
|
|
@@ -668,40 +669,25 @@ export default class QueryHelper {
|
|
|
668
669
|
) as FindWhereProperty<any>;
|
|
669
670
|
}
|
|
670
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Filter on the keys of a jsonb column — `customFields` above all, whose
|
|
674
|
+
* keys are the names a project gave its custom fields.
|
|
675
|
+
*
|
|
676
|
+
* The per-key predicates (equality, "is any of", contains, numeric
|
|
677
|
+
* comparison, is-empty, and their negations) live in JSONColumnQuery. Two
|
|
678
|
+
* things this used to get wrong and no longer does: the key was spliced
|
|
679
|
+
* straight into the SQL string, and every value was compared with `=`, so an
|
|
680
|
+
* operator such as `Includes` reached Postgres stringified.
|
|
681
|
+
*/
|
|
671
682
|
@CaptureSpan()
|
|
672
683
|
public static queryJson(value: JSONObject): FindWhereProperty<any> {
|
|
673
|
-
// seed random text
|
|
674
|
-
const values: JSONObject = {};
|
|
675
|
-
|
|
676
|
-
let queryText: string = "";
|
|
677
|
-
|
|
678
|
-
if (typeof value === Typeof.String) {
|
|
679
|
-
value = JSON.parse(value.toString());
|
|
680
|
-
}
|
|
681
|
-
|
|
682
684
|
if (value instanceof FindOperator) {
|
|
683
685
|
return value;
|
|
684
686
|
}
|
|
685
687
|
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const temp: string = Text.generateRandomText(10);
|
|
690
|
-
|
|
691
|
-
values[temp] = value[key];
|
|
692
|
-
|
|
693
|
-
if (!queryText) {
|
|
694
|
-
queryText = `(COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
|
|
695
|
-
} else {
|
|
696
|
-
queryText += ` AND COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (hasValue) {
|
|
701
|
-
queryText += ")";
|
|
702
|
-
} else {
|
|
703
|
-
queryText = "(COLUMN_NAME_ALIAS IS NULL OR COLUMN_NAME_ALIAS = '{}')";
|
|
704
|
-
}
|
|
688
|
+
const jsonQuery: JSONColumnQuery = buildJSONColumnQuery({
|
|
689
|
+
value: value as JSONObject,
|
|
690
|
+
});
|
|
705
691
|
|
|
706
692
|
return Raw((alias: string) => {
|
|
707
693
|
/*
|
|
@@ -709,16 +695,15 @@ export default class QueryHelper {
|
|
|
709
695
|
* we need to convert this to "tableName"."columnName"
|
|
710
696
|
*/
|
|
711
697
|
|
|
712
|
-
|
|
698
|
+
const columnReference: string = alias
|
|
713
699
|
.split(".")
|
|
714
700
|
.map((item: string) => {
|
|
715
701
|
return `"${item}"`;
|
|
716
702
|
})
|
|
717
703
|
.join(".");
|
|
718
704
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}, values);
|
|
705
|
+
return jsonQuery.toSql(columnReference);
|
|
706
|
+
}, jsonQuery.parameters);
|
|
722
707
|
}
|
|
723
708
|
|
|
724
709
|
@CaptureSpan()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* TruncateColumnValue
|
|
6
|
+
*
|
|
7
|
+
* Per-value clamps for bounded varchar columns written by the bulk
|
|
8
|
+
* inventory upsert paths (Kubernetes / Docker / Docker Swarm / Podman /
|
|
9
|
+
* Proxmox / Ceph / IoT).
|
|
10
|
+
*
|
|
11
|
+
* Those services hand-build `INSERT ... ON CONFLICT` statements and
|
|
12
|
+
* flush them in chunks of 500 rows through
|
|
13
|
+
* `getRepository().manager.query(...)`, which bypasses the length
|
|
14
|
+
* validation DatabaseService applies to ordinary CRUD writes. A single
|
|
15
|
+
* value past its column limit raises 22001 ("value too long for type
|
|
16
|
+
* character varying(N)") and aborts the WHOLE multi-row INSERT, so one
|
|
17
|
+
* malformed resource silently drops up to 499 healthy inventory rows
|
|
18
|
+
* with it. Clamping per value keeps the blast radius at one field.
|
|
19
|
+
*
|
|
20
|
+
* Truncation is the last line of defense, not the primary fix: columns
|
|
21
|
+
* that legitimately carry long values (image references, Kubernetes
|
|
22
|
+
* DNS-subdomain names) should be widened in the model + a migration so
|
|
23
|
+
* these clamps effectively never fire.
|
|
24
|
+
* ------------------------------------------------------------------
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export function truncateToLength(value: string, maxLength: number): string;
|
|
28
|
+
export function truncateToLength(
|
|
29
|
+
value: string | null | undefined,
|
|
30
|
+
maxLength: number,
|
|
31
|
+
): string | null;
|
|
32
|
+
export function truncateToLength(
|
|
33
|
+
value: string | null | undefined,
|
|
34
|
+
maxLength: number,
|
|
35
|
+
): string | null {
|
|
36
|
+
if (value === null || value === undefined) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value.length <= maxLength) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const clipped: string = value.substring(0, maxLength);
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* JS string length counts UTF-16 code units, so a cut at maxLength can
|
|
48
|
+
* land in the middle of a surrogate pair (any astral-plane character:
|
|
49
|
+
* emoji, rare CJK). The orphaned high surrogate is not valid UTF-8 —
|
|
50
|
+
* Node encodes it as U+FFFD on the way to the driver — so drop it.
|
|
51
|
+
* A well-formed string never ends a character on a high surrogate,
|
|
52
|
+
* which makes this check unambiguous. Postgres counts characters, not
|
|
53
|
+
* code units, so clipping by code units is always conservative: the
|
|
54
|
+
* result can only be shorter than the column bound, never longer.
|
|
55
|
+
*/
|
|
56
|
+
const lastCodeUnit: number = clipped.charCodeAt(clipped.length - 1);
|
|
57
|
+
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) {
|
|
58
|
+
return clipped.substring(0, clipped.length - 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return clipped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clamp to ColumnLength.ShortText — varchar(100). */
|
|
65
|
+
export function truncateShortText(value: string): string;
|
|
66
|
+
export function truncateShortText(
|
|
67
|
+
value: string | null | undefined,
|
|
68
|
+
): string | null;
|
|
69
|
+
export function truncateShortText(
|
|
70
|
+
value: string | null | undefined,
|
|
71
|
+
): string | null {
|
|
72
|
+
return truncateToLength(value, ColumnLength.ShortText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Clamp to ColumnLength.LongText — varchar(500). */
|
|
76
|
+
export function truncateLongText(value: string): string;
|
|
77
|
+
export function truncateLongText(
|
|
78
|
+
value: string | null | undefined,
|
|
79
|
+
): string | null;
|
|
80
|
+
export function truncateLongText(
|
|
81
|
+
value: string | null | undefined,
|
|
82
|
+
): string | null {
|
|
83
|
+
return truncateToLength(value, ColumnLength.LongText);
|
|
84
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import React, { ReactElement } from "react";
|
|
4
|
+
import {
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
jest,
|
|
10
|
+
test,
|
|
11
|
+
} from "@jest/globals";
|
|
12
|
+
import { MemoryRouter } from "react-router-dom";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The customer-facing bug this whole change exists for: Settings > Users
|
|
16
|
+
* listed TeamMember, a (user, team) join row, so one person on three teams
|
|
17
|
+
* filled three rows with the same name and avatar and read as three different
|
|
18
|
+
* people.
|
|
19
|
+
*
|
|
20
|
+
* The unit suites cover the grouping util and the API class. Neither of them
|
|
21
|
+
* touches the page, and the fix on the page is a single prop -
|
|
22
|
+
* modelAPI={ProjectUsersModelAPI} - whose loss brings every duplicate straight
|
|
23
|
+
* back with no test failing. That prop, the column that now has to render a
|
|
24
|
+
* list of teams rather than one, and the row's derived status are what this
|
|
25
|
+
* file pins.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
jest.mock("../../../UI/Utils/Permission", () => {
|
|
29
|
+
return {
|
|
30
|
+
__esModule: true,
|
|
31
|
+
default: {
|
|
32
|
+
getAllPermissions: () => {
|
|
33
|
+
return [];
|
|
34
|
+
},
|
|
35
|
+
getProjectPermissions: () => {
|
|
36
|
+
return [];
|
|
37
|
+
},
|
|
38
|
+
getGlobalPermissions: () => {
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
jest.mock("../../../UI/Utils/User", () => {
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: {
|
|
49
|
+
isMasterAdmin: () => {
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
getUserId: () => {
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
jest.mock("../../../UI/Utils/Translation", () => {
|
|
60
|
+
return {
|
|
61
|
+
__esModule: true,
|
|
62
|
+
default: () => {
|
|
63
|
+
return {
|
|
64
|
+
translateString: (value: string | undefined) => {
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
translateValue: (value: unknown) => {
|
|
68
|
+
return value;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The page is rendered through a stand-in ModelTable that records the props it
|
|
77
|
+
* was given. Rendering the real one would drag in the facet bar, the URL
|
|
78
|
+
* state and the pager, none of which this file is about - what matters is
|
|
79
|
+
* which API the page hands the table, and what its columns do with a grouped
|
|
80
|
+
* row.
|
|
81
|
+
*/
|
|
82
|
+
type CapturedTableProps = {
|
|
83
|
+
modelAPI?: unknown;
|
|
84
|
+
columns: Array<{
|
|
85
|
+
title: string;
|
|
86
|
+
disableSort?: boolean | undefined;
|
|
87
|
+
getElement?: ((item: TeamMember) => ReactElement) | undefined;
|
|
88
|
+
getExportValue?: ((item: TeamMember) => string) | undefined;
|
|
89
|
+
}>;
|
|
90
|
+
deleteButtonText?: string | undefined;
|
|
91
|
+
singularName?: string | undefined;
|
|
92
|
+
pluralName?: string | undefined;
|
|
93
|
+
isDeleteable?: boolean | undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let capturedTableProps: CapturedTableProps | null = null;
|
|
97
|
+
|
|
98
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
99
|
+
return {
|
|
100
|
+
__esModule: true,
|
|
101
|
+
default: (props: CapturedTableProps) => {
|
|
102
|
+
capturedTableProps = props;
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
import Users from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/Index";
|
|
109
|
+
import ProjectUsersModelAPI from "../../../UI/Utils/ModelAPI/ProjectUsersModelAPI";
|
|
110
|
+
import TeamMembersByUser, {
|
|
111
|
+
ProjectUserRow,
|
|
112
|
+
} from "../../../UI/Utils/TeamMembersByUser";
|
|
113
|
+
import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
|
|
114
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
115
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
116
|
+
import Team from "../../../Models/DatabaseModels/Team";
|
|
117
|
+
import TeamMember from "../../../Models/DatabaseModels/TeamMember";
|
|
118
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
119
|
+
import Route from "../../../Types/API/Route";
|
|
120
|
+
import Name from "../../../Types/Name";
|
|
121
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
122
|
+
|
|
123
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
124
|
+
"00000000-0000-4000-8000-000000000001",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const currentProject: Project = new Project();
|
|
128
|
+
currentProject._id = PROJECT_ID.toString();
|
|
129
|
+
|
|
130
|
+
type MembershipSpec = {
|
|
131
|
+
id: string;
|
|
132
|
+
userId: string;
|
|
133
|
+
userName?: string | undefined;
|
|
134
|
+
teamId?: string | undefined;
|
|
135
|
+
teamName?: string | undefined;
|
|
136
|
+
hasAcceptedInvitation?: boolean | undefined;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const buildMembership: (spec: MembershipSpec) => TeamMember = (
|
|
140
|
+
spec: MembershipSpec,
|
|
141
|
+
): TeamMember => {
|
|
142
|
+
const teamMember: TeamMember = new TeamMember();
|
|
143
|
+
|
|
144
|
+
teamMember._id = spec.id;
|
|
145
|
+
teamMember.userId = new ObjectID(spec.userId);
|
|
146
|
+
teamMember.projectId = PROJECT_ID;
|
|
147
|
+
teamMember.hasAcceptedInvitation = spec.hasAcceptedInvitation ?? true;
|
|
148
|
+
|
|
149
|
+
const user: User = new User();
|
|
150
|
+
user._id = spec.userId;
|
|
151
|
+
|
|
152
|
+
if (spec.userName) {
|
|
153
|
+
user.name = new Name(spec.userName);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
teamMember.user = user;
|
|
157
|
+
|
|
158
|
+
if (spec.teamId) {
|
|
159
|
+
teamMember.teamId = new ObjectID(spec.teamId);
|
|
160
|
+
|
|
161
|
+
const team: Team = new Team();
|
|
162
|
+
team._id = spec.teamId;
|
|
163
|
+
team.name = spec.teamName || spec.teamId;
|
|
164
|
+
teamMember.team = team;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return teamMember;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* A row exactly as ProjectUsersModelAPI hands it to the table, built by the
|
|
172
|
+
* real grouping code rather than hand-written, so a change in the shape of a
|
|
173
|
+
* grouped row cannot leave these assertions passing against a shape the page
|
|
174
|
+
* never actually receives.
|
|
175
|
+
*/
|
|
176
|
+
const buildGroupedRow: (
|
|
177
|
+
memberships: Array<MembershipSpec>,
|
|
178
|
+
) => ProjectUserRow = (memberships: Array<MembershipSpec>): ProjectUserRow => {
|
|
179
|
+
return TeamMembersByUser.groupByUser(memberships.map(buildMembership))[0]!;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
type ColumnOf = (title: string) => CapturedTableProps["columns"][0];
|
|
183
|
+
|
|
184
|
+
const columnTitled: ColumnOf = (
|
|
185
|
+
title: string,
|
|
186
|
+
): CapturedTableProps["columns"][0] => {
|
|
187
|
+
const column: CapturedTableProps["columns"][0] | undefined =
|
|
188
|
+
capturedTableProps?.columns.find(
|
|
189
|
+
(candidate: CapturedTableProps["columns"][0]) => {
|
|
190
|
+
return candidate.title === title;
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (!column) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`No "${title}" column. Columns: ${(capturedTableProps?.columns || [])
|
|
197
|
+
.map((candidate: CapturedTableProps["columns"][0]) => {
|
|
198
|
+
return candidate.title;
|
|
199
|
+
})
|
|
200
|
+
.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return column;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
type RenderCellFunction = (title: string, row: ProjectUserRow) => void;
|
|
208
|
+
|
|
209
|
+
const renderCell: RenderCellFunction = (
|
|
210
|
+
title: string,
|
|
211
|
+
row: ProjectUserRow,
|
|
212
|
+
): void => {
|
|
213
|
+
render(<MemoryRouter>{columnTitled(title).getElement!(row)}</MemoryRouter>);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
describe("Settings > Users page", () => {
|
|
217
|
+
beforeEach(async () => {
|
|
218
|
+
capturedTableProps = null;
|
|
219
|
+
|
|
220
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
221
|
+
// The page counts SCIM configs on mount to decide whether to warn.
|
|
222
|
+
jest.spyOn(ModelAPI, "count").mockResolvedValue(0);
|
|
223
|
+
jest.spyOn(ModelAPI, "getCommonHeaders").mockReturnValue({});
|
|
224
|
+
|
|
225
|
+
render(
|
|
226
|
+
<MemoryRouter>
|
|
227
|
+
<Users
|
|
228
|
+
pageRoute={new Route("/dashboard/users")}
|
|
229
|
+
currentProject={currentProject}
|
|
230
|
+
hasPaymentMethod={true}
|
|
231
|
+
/>
|
|
232
|
+
</MemoryRouter>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect(capturedTableProps).not.toBeNull();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
afterEach(() => {
|
|
241
|
+
jest.restoreAllMocks();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("the fix itself", () => {
|
|
245
|
+
/*
|
|
246
|
+
* If this fails, every user on more than one team is back to occupying one
|
|
247
|
+
* row per team - which is the bug customers reported.
|
|
248
|
+
*/
|
|
249
|
+
test("lists people, not memberships, by using the grouping API", () => {
|
|
250
|
+
expect(capturedTableProps!.modelAPI).toBe(ProjectUsersModelAPI);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("names its rows after people", () => {
|
|
254
|
+
expect(capturedTableProps!.singularName).toBe("User");
|
|
255
|
+
expect(capturedTableProps!.pluralName).toBe("Users");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("the Teams column", () => {
|
|
260
|
+
test("shows every team the person belongs to, not just one", () => {
|
|
261
|
+
renderCell(
|
|
262
|
+
"Teams",
|
|
263
|
+
buildGroupedRow([
|
|
264
|
+
{
|
|
265
|
+
id: "m1",
|
|
266
|
+
userId: "u1",
|
|
267
|
+
userName: "Thomas Stock",
|
|
268
|
+
teamId: "t1",
|
|
269
|
+
teamName: "NTW_Online Operations",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "m2",
|
|
273
|
+
userId: "u1",
|
|
274
|
+
userName: "Thomas Stock",
|
|
275
|
+
teamId: "t2",
|
|
276
|
+
teamName: "Owners",
|
|
277
|
+
},
|
|
278
|
+
]),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(screen.getByText("NTW_Online Operations")).toBeInTheDocument();
|
|
282
|
+
expect(screen.getByText("Owners")).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("collapses a long team list behind a 'more' control rather than growing the row", () => {
|
|
286
|
+
renderCell(
|
|
287
|
+
"Teams",
|
|
288
|
+
buildGroupedRow([
|
|
289
|
+
{ id: "m1", userId: "u1", teamId: "t1", teamName: "Alpha" },
|
|
290
|
+
{ id: "m2", userId: "u1", teamId: "t2", teamName: "Bravo" },
|
|
291
|
+
{ id: "m3", userId: "u1", teamId: "t3", teamName: "Charlie" },
|
|
292
|
+
{ id: "m4", userId: "u1", teamId: "t4", teamName: "Delta" },
|
|
293
|
+
{ id: "m5", userId: "u1", teamId: "t5", teamName: "Echo" },
|
|
294
|
+
]),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(screen.getByText("Alpha")).toBeInTheDocument();
|
|
298
|
+
expect(screen.queryByText("Echo")).not.toBeInTheDocument();
|
|
299
|
+
expect(screen.getByText("2 more teams")).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("says so when the person is on no team at all", () => {
|
|
303
|
+
renderCell(
|
|
304
|
+
"Teams",
|
|
305
|
+
buildGroupedRow([{ id: "m1", userId: "u1", userName: "Nobody" }]),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(screen.getByText("No team assigned")).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/*
|
|
312
|
+
* The exporter is column-driven and would otherwise fall back to the
|
|
313
|
+
* single `team` on the membership the row was built from, so the CSV would
|
|
314
|
+
* quietly disagree with the cell beside it.
|
|
315
|
+
*/
|
|
316
|
+
test("exports every team, not the one the row was built from", () => {
|
|
317
|
+
const row: ProjectUserRow = buildGroupedRow([
|
|
318
|
+
{ id: "m1", userId: "u1", teamId: "t1", teamName: "Engineering" },
|
|
319
|
+
{ id: "m2", userId: "u1", teamId: "t2", teamName: "On-Call" },
|
|
320
|
+
{ id: "m3", userId: "u1", teamId: "t3", teamName: "Admins" },
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
expect(columnTitled("Teams").getExportValue!(row)).toBe(
|
|
324
|
+
"Admins; Engineering; On-Call",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("exports an empty cell rather than crashing on a team-less row", () => {
|
|
329
|
+
const row: ProjectUserRow = buildGroupedRow([
|
|
330
|
+
{ id: "m1", userId: "u1", userName: "Nobody" },
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
expect(columnTitled("Teams").getExportValue!(row)).toBe("");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("the Status column", () => {
|
|
338
|
+
test("reads Member once any invitation has been accepted", () => {
|
|
339
|
+
renderCell(
|
|
340
|
+
"Status",
|
|
341
|
+
buildGroupedRow([
|
|
342
|
+
{
|
|
343
|
+
id: "m1",
|
|
344
|
+
userId: "u1",
|
|
345
|
+
teamId: "t1",
|
|
346
|
+
hasAcceptedInvitation: true,
|
|
347
|
+
},
|
|
348
|
+
]),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(screen.getByText("Member")).toBeInTheDocument();
|
|
352
|
+
expect(screen.queryByText(/Pending/)).not.toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("reads Invitation Sent while nothing has been accepted", () => {
|
|
356
|
+
renderCell(
|
|
357
|
+
"Status",
|
|
358
|
+
buildGroupedRow([
|
|
359
|
+
{
|
|
360
|
+
id: "m1",
|
|
361
|
+
userId: "u1",
|
|
362
|
+
teamId: "t1",
|
|
363
|
+
hasAcceptedInvitation: false,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: "m2",
|
|
367
|
+
userId: "u1",
|
|
368
|
+
teamId: "t2",
|
|
369
|
+
hasAcceptedInvitation: false,
|
|
370
|
+
},
|
|
371
|
+
]),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(screen.getByText("Invitation Sent")).toBeInTheDocument();
|
|
375
|
+
expect(screen.queryByText("Member")).not.toBeInTheDocument();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
* Per-team invitation state used to be visible because each row WAS one
|
|
380
|
+
* team. On a grouped row it would disappear entirely without this.
|
|
381
|
+
*/
|
|
382
|
+
test("still surfaces invitations the person has not accepted yet", () => {
|
|
383
|
+
renderCell(
|
|
384
|
+
"Status",
|
|
385
|
+
buildGroupedRow([
|
|
386
|
+
{
|
|
387
|
+
id: "m1",
|
|
388
|
+
userId: "u1",
|
|
389
|
+
teamId: "t1",
|
|
390
|
+
hasAcceptedInvitation: true,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "m2",
|
|
394
|
+
userId: "u1",
|
|
395
|
+
teamId: "t2",
|
|
396
|
+
hasAcceptedInvitation: false,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "m3",
|
|
400
|
+
userId: "u1",
|
|
401
|
+
teamId: "t3",
|
|
402
|
+
hasAcceptedInvitation: false,
|
|
403
|
+
},
|
|
404
|
+
]),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(screen.getByText("Member")).toBeInTheDocument();
|
|
408
|
+
expect(screen.getByText("2 Invitations Pending")).toBeInTheDocument();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("counts a single pending invitation in the singular", () => {
|
|
412
|
+
renderCell(
|
|
413
|
+
"Status",
|
|
414
|
+
buildGroupedRow([
|
|
415
|
+
{
|
|
416
|
+
id: "m1",
|
|
417
|
+
userId: "u1",
|
|
418
|
+
teamId: "t1",
|
|
419
|
+
hasAcceptedInvitation: true,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: "m2",
|
|
423
|
+
userId: "u1",
|
|
424
|
+
teamId: "t2",
|
|
425
|
+
hasAcceptedInvitation: false,
|
|
426
|
+
},
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(screen.getByText("1 Invitation Pending")).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
/*
|
|
434
|
+
* The server can only order by the stored per-membership column, which on
|
|
435
|
+
* a grouped row disagrees with the derived one on screen - so the control
|
|
436
|
+
* has to be off rather than visibly not sorting.
|
|
437
|
+
*/
|
|
438
|
+
test("offers no sort control, because the value is derived", () => {
|
|
439
|
+
expect(columnTitled("Status").disableSort).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("removing someone", () => {
|
|
444
|
+
test("labels the action as removal from the project, not from a team", () => {
|
|
445
|
+
expect(capturedTableProps!.deleteButtonText).toBe("Remove from Project");
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|