@oneuptime/common 12.0.31 → 12.0.32

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 (188) hide show
  1. package/Models/DatabaseModels/AlertState.ts +0 -2
  2. package/Models/DatabaseModels/Domain.ts +1 -1
  3. package/Models/DatabaseModels/Incident.ts +1 -1
  4. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/MetricType.ts +0 -2
  7. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  8. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  9. package/Models/DatabaseModels/NetworkDevice.ts +121 -2
  10. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  11. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  13. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  14. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  15. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  16. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  17. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  18. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  21. package/Server/Services/DatabaseService.ts +39 -9
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  24. package/Server/Services/ProjectService.ts +85 -0
  25. package/Server/Services/UserNotificationSettingService.ts +0 -6
  26. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  27. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  28. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  29. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  30. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  31. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  32. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  33. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  34. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  35. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  36. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  37. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  38. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  39. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  40. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  41. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  42. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  43. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  44. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  45. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  46. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  47. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  48. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  49. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  50. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  51. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  52. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  53. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  54. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  55. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  56. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  57. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  58. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  59. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  60. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  61. package/Tests/Utils/Slug.test.ts +61 -1
  62. package/Tests/jest.setup.ts +19 -0
  63. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  64. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  65. package/Types/Permission.ts +45 -0
  66. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  67. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  68. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  69. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  70. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  71. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  72. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  73. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  74. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  75. package/UI/Components/Navbar/NavBar.tsx +62 -13
  76. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  77. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  78. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  79. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  80. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  81. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  82. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  83. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  86. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  87. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  88. package/Utils/Slug.ts +34 -1
  89. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  90. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  92. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  94. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  96. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  100. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  102. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  104. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  105. package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
  106. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  107. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  108. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  109. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  110. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  111. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  112. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  114. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  116. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  118. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  120. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  122. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  124. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  129. package/build/dist/Server/Services/DatabaseService.js +31 -5
  130. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  131. package/build/dist/Server/Services/Index.js +2 -0
  132. package/build/dist/Server/Services/Index.js.map +1 -1
  133. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  134. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  135. package/build/dist/Server/Services/ProjectService.js +65 -0
  136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  138. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  139. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  140. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  141. package/build/dist/Types/Permission.js +40 -0
  142. package/build/dist/Types/Permission.js.map +1 -1
  143. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  144. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  145. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  146. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  147. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  148. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  149. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  150. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  151. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  152. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  153. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  154. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  155. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  156. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  157. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  158. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  159. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  160. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  161. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  162. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  163. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  164. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  165. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  166. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  167. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  168. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  169. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  170. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  171. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  172. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  173. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  174. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  175. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  176. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  178. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  179. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  181. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  183. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  184. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  185. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  186. package/build/dist/Utils/Slug.js +31 -1
  187. package/build/dist/Utils/Slug.js.map +1 -1
  188. package/package.json +1 -1
@@ -14,7 +14,6 @@ import ColumnType from "../../Types/Database/ColumnType";
14
14
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
15
15
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
16
16
  import EnableWorkflow from "../../Types/Database/EnableWorkflow";
17
- import SlugifyColumn from "../../Types/Database/SlugifyColumn";
18
17
  import TableColumn from "../../Types/Database/TableColumn";
19
18
  import TableColumnType from "../../Types/Database/TableColumnType";
20
19
  import TableMetadata from "../../Types/Database/TableMetadata";
@@ -78,7 +77,6 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
78
77
  read: true,
79
78
  })
80
79
  @CrudApiEndpoint(new Route("/status-page-private-user"))
81
- @SlugifyColumn("name", "slug")
82
80
  @TableMetadata({
83
81
  tableName: "StatusPagePrivateUser",
84
82
  singularName: "Status Page Private User",
@@ -15,7 +15,6 @@ import ColumnType from "../../Types/Database/ColumnType";
15
15
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
16
16
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
17
17
  import EnableWorkflow from "../../Types/Database/EnableWorkflow";
18
- import SlugifyColumn from "../../Types/Database/SlugifyColumn";
19
18
  import TableColumn from "../../Types/Database/TableColumn";
20
19
  import TableColumnType from "../../Types/Database/TableColumnType";
21
20
  import TableMetadata from "../../Types/Database/TableMetadata";
@@ -72,7 +71,6 @@ import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
72
71
  read: true,
73
72
  })
74
73
  @CrudApiEndpoint(new Route("/status-page-resource"))
75
- @SlugifyColumn("name", "slug")
76
74
  @TableMetadata({
77
75
  tableName: "StatusPageResource",
78
76
  singularName: "Status Page Resource",
@@ -14,7 +14,6 @@ import ColumnType from "../../Types/Database/ColumnType";
14
14
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
15
15
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
16
16
  import EnableWorkflow from "../../Types/Database/EnableWorkflow";
17
- import SlugifyColumn from "../../Types/Database/SlugifyColumn";
18
17
  import TableColumn from "../../Types/Database/TableColumn";
19
18
  import TableColumnType from "../../Types/Database/TableColumnType";
20
19
  import TableMetadata from "../../Types/Database/TableMetadata";
@@ -82,7 +81,6 @@ import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
82
81
  ],
83
82
  })
84
83
  @CrudApiEndpoint(new Route("/status-page-subscriber"))
85
- @SlugifyColumn("name", "slug")
86
84
  @TableMetadata({
87
85
  tableName: "StatusPageSubscriber",
88
86
  singularName: "Status Page Subscriber",
@@ -0,0 +1,72 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Device roles become a per-project lookup instead of a fixed union.
5
+ *
6
+ * "deviceRole" (the inline string on NetworkDevice) is NOT touched here: the
7
+ * BackfillNetworkDeviceRoles data migration still reads it to point every
8
+ * device that has one at its new NetworkDeviceRole row. It is dropped in a
9
+ * follow-up once that backfill has run everywhere.
10
+ *
11
+ * networkDeviceRoleId is ON DELETE SET NULL on purpose. Deleting a role from
12
+ * the project's settings must not delete the devices using it - they simply go
13
+ * back to being classified from their own SNMP identity, which is exactly what
14
+ * a NULL role has always meant.
15
+ */
16
+ export class AddNetworkDeviceRoleTable1790800000000
17
+ implements MigrationInterface
18
+ {
19
+ public name: string = "AddNetworkDeviceRoleTable1790800000000";
20
+
21
+ public async up(queryRunner: QueryRunner): Promise<void> {
22
+ await queryRunner.query(
23
+ `CREATE TABLE "NetworkDeviceRole" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "slug" character varying(100) NOT NULL, "key" character varying(100) NOT NULL, "description" character varying(500), "topologyShape" character varying(100), "isCoreLayer" boolean NOT NULL DEFAULT false, "isSnmpWalkable" boolean NOT NULL DEFAULT true, "order" integer, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_10fb79a5d66950e4a9c6d774d07" PRIMARY KEY ("_id"))`,
24
+ );
25
+ await queryRunner.query(
26
+ `CREATE INDEX "IDX_188009b80d1281f1d7ccecdf74" ON "NetworkDeviceRole" ("projectId") `,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "NetworkDevice" ADD "networkDeviceRoleId" uuid`,
30
+ );
31
+ await queryRunner.query(
32
+ `CREATE INDEX "IDX_41f59fed52c5ec4ff6a2725ad9" ON "NetworkDevice" ("networkDeviceRoleId") `,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "NetworkDeviceRole" ADD CONSTRAINT "FK_188009b80d1281f1d7ccecdf74d" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "NetworkDeviceRole" ADD CONSTRAINT "FK_9860e7a5b4d23cf1c4192c468cd" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
39
+ );
40
+ await queryRunner.query(
41
+ `ALTER TABLE "NetworkDeviceRole" ADD CONSTRAINT "FK_9dc66fa19636ba160af1521f45d" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
42
+ );
43
+ await queryRunner.query(
44
+ `ALTER TABLE "NetworkDevice" ADD CONSTRAINT "FK_41f59fed52c5ec4ff6a2725ad9a" FOREIGN KEY ("networkDeviceRoleId") REFERENCES "NetworkDeviceRole"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
45
+ );
46
+ }
47
+
48
+ public async down(queryRunner: QueryRunner): Promise<void> {
49
+ await queryRunner.query(
50
+ `ALTER TABLE "NetworkDevice" DROP CONSTRAINT "FK_41f59fed52c5ec4ff6a2725ad9a"`,
51
+ );
52
+ await queryRunner.query(
53
+ `ALTER TABLE "NetworkDeviceRole" DROP CONSTRAINT "FK_9dc66fa19636ba160af1521f45d"`,
54
+ );
55
+ await queryRunner.query(
56
+ `ALTER TABLE "NetworkDeviceRole" DROP CONSTRAINT "FK_9860e7a5b4d23cf1c4192c468cd"`,
57
+ );
58
+ await queryRunner.query(
59
+ `ALTER TABLE "NetworkDeviceRole" DROP CONSTRAINT "FK_188009b80d1281f1d7ccecdf74d"`,
60
+ );
61
+ await queryRunner.query(
62
+ `DROP INDEX "public"."IDX_41f59fed52c5ec4ff6a2725ad9"`,
63
+ );
64
+ await queryRunner.query(
65
+ `ALTER TABLE "NetworkDevice" DROP COLUMN "networkDeviceRoleId"`,
66
+ );
67
+ await queryRunner.query(
68
+ `DROP INDEX "public"."IDX_188009b80d1281f1d7ccecdf74"`,
69
+ );
70
+ await queryRunner.query(`DROP TABLE "NetworkDeviceRole"`);
71
+ }
72
+ }
@@ -566,6 +566,7 @@ import { AddCertificateReissueRequestedAtToDomains1790300000000 } from "./179030
566
566
  import { AddOnCallCalendarFeeds1790400000000 } from "./1790400000000-AddOnCallCalendarFeeds";
567
567
  import { AddNetworkDeviceOidTemplate1790500000000 } from "./1790500000000-AddNetworkDeviceOidTemplate";
568
568
  import { AddAutoImportRuleOidTemplate1790600000000 } from "./1790600000000-AddAutoImportRuleOidTemplate";
569
+ import { AddNetworkDeviceRoleTable1790800000000 } from "./1790800000000-AddNetworkDeviceRoleTable";
569
570
 
570
571
  export default [
571
572
  InitialMigration,
@@ -1136,4 +1137,5 @@ export default [
1136
1137
  AddOnCallCalendarFeeds1790400000000,
1137
1138
  AddNetworkDeviceOidTemplate1790500000000,
1138
1139
  AddAutoImportRuleOidTemplate1790600000000,
1140
+ AddNetworkDeviceRoleTable1790800000000,
1139
1141
  ];
@@ -616,21 +616,51 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
616
616
  throw PostgresErrorTranslator.translateException(error);
617
617
  }
618
618
 
619
+ /*
620
+ * Both halves of @SlugifyColumn are resolved BY NAME off the object rather
621
+ * than through the schema, so a name matching no declared column misfires
622
+ * silently: a missing source reads `undefined` and Slug.getSlug answers with
623
+ * a random Faker name, and a missing destination is assigned to a property
624
+ * TypeORM drops on insert. ModelRegistryInvariants sweeps every model for
625
+ * both, so the pairing is guaranteed by a test rather than by this code.
626
+ *
627
+ * The ceiling comes from the DESTINATION column, not from Slug: getSlug
628
+ * appends a dash and ten random digits, and checkMaxLengthOfFields -- which
629
+ * runs later in the same create -- THROWS on overflow rather than
630
+ * truncating. Sources are routinely wider than the slug they feed
631
+ * (Incident.title is varchar(500) against a varchar(100) slug), so without
632
+ * this a long title would be a failed insert rather than a long slug.
633
+ */
619
634
  private generateSlug(createBy: CreateBy<TBaseModel>): CreateBy<TBaseModel> {
620
- if (createBy.data.getSlugifyColumn()) {
621
- (createBy.data as any)[createBy.data.getSaveSlugToColumn() as string] =
622
- Slug.getSlug(
623
- (createBy.data as any)[createBy.data.getSlugifyColumn() as string]
624
- ? ((createBy.data as any)[
625
- createBy.data.getSlugifyColumn() as string
626
- ] as string)
627
- : null,
628
- );
635
+ const slugifyColumn: string | null = createBy.data.getSlugifyColumn();
636
+ const saveSlugToColumn: string | null = createBy.data.getSaveSlugToColumn();
637
+
638
+ if (!slugifyColumn || !saveSlugToColumn) {
639
+ return createBy;
629
640
  }
630
641
 
642
+ const source: JSONValue = (createBy.data as any)[slugifyColumn];
643
+
644
+ (createBy.data as any)[saveSlugToColumn] = Slug.getSlug(
645
+ source ? String(source) : null,
646
+ this.getMaxLengthOfColumn(saveSlugToColumn),
647
+ );
648
+
631
649
  return createBy;
632
650
  }
633
651
 
652
+ /*
653
+ * The declared width of a column, or undefined for a column that declares
654
+ * none -- including a name that is not a column at all, which is what
655
+ * getTableColumnMetadata answers for.
656
+ */
657
+ private getMaxLengthOfColumn(columnName: string): number | undefined {
658
+ const columnType: TableColumnType | undefined =
659
+ this.model.getTableColumnMetadata(columnName)?.type;
660
+
661
+ return columnType ? getMaxLengthFromTableColumnType(columnType) : undefined;
662
+ }
663
+
634
664
  private async sanitizeCreateOrUpdate(
635
665
  data: TBaseModel | PartialEntity<TBaseModel>,
636
666
  props: DatabaseCommonInteractionProps,
@@ -62,6 +62,7 @@ import NetworkDeviceOwnerRuleService from "./NetworkDeviceOwnerRuleService";
62
62
  import NetworkDeviceLabelRuleService from "./NetworkDeviceLabelRuleService";
63
63
  import NetworkDeviceAutoImportRuleService from "./NetworkDeviceAutoImportRuleService";
64
64
  import NetworkDeviceOidTemplateService from "./NetworkDeviceOidTemplateService";
65
+ import NetworkDeviceRoleService from "./NetworkDeviceRoleService";
65
66
  import NetworkDeviceDiscoveryScanService from "./NetworkDeviceDiscoveryScanService";
66
67
  import NetworkInterfaceService from "./NetworkInterfaceService";
67
68
  import NetworkSiteService from "./NetworkSiteService";
@@ -508,6 +509,7 @@ const services: Array<BaseService> = [
508
509
  NetworkDeviceLabelRuleService,
509
510
  NetworkDeviceAutoImportRuleService,
510
511
  NetworkDeviceOidTemplateService,
512
+ NetworkDeviceRoleService,
511
513
  NetworkDeviceDiscoveryScanService,
512
514
  NetworkInterfaceService,
513
515
  NetworkSiteService,
@@ -0,0 +1,90 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/NetworkDeviceRole";
3
+ import CreateBy from "../Types/Database/CreateBy";
4
+ import { OnCreate } from "../Types/Database/Hooks";
5
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
6
+ import ObjectID from "../../Types/ObjectID";
7
+ import BadDataException from "../../Types/Exception/BadDataException";
8
+ import { buildUniqueDeviceRoleKey } from "../../Utils/NetworkDevice/DeviceRoleKeyUtil";
9
+
10
+ export class Service extends DatabaseService<Model> {
11
+ public constructor() {
12
+ super(Model);
13
+ }
14
+
15
+ /*
16
+ * Every role gets its stable key here, and only here.
17
+ *
18
+ * The key is not a form field - an operator names a role, they do not name
19
+ * its identifier - and it must never change afterwards, because it is what
20
+ * the SNMP classifier's answer and every stored topology payload match
21
+ * against. Deriving it at create time is therefore the whole of its
22
+ * lifecycle: the column's ColumnAccessControl grants create and update to
23
+ * nobody, so the API cannot set or move it either.
24
+ *
25
+ * The seeders pass an explicit key so the eleven built-in roles keep the
26
+ * exact identifiers the classifier emits ("wirelessAccessPoint" from
27
+ * "Wireless AP", which no derivation would produce). Anything else derives
28
+ * one from the name, de-duplicated against the keys the project already
29
+ * holds.
30
+ */
31
+ protected override async onBeforeCreate(
32
+ createBy: CreateBy<Model>,
33
+ ): Promise<OnCreate<Model>> {
34
+ if (!createBy.data.key) {
35
+ const projectId: ObjectID | undefined =
36
+ createBy.data.projectId || createBy.props.tenantId || undefined;
37
+
38
+ if (!projectId) {
39
+ throw new BadDataException(
40
+ "Project ID is required to create a network device role.",
41
+ );
42
+ }
43
+
44
+ createBy.data.key = buildUniqueDeviceRoleKey(
45
+ createBy.data.name || "",
46
+ await this.getKeysInProject(projectId),
47
+ );
48
+ }
49
+
50
+ return {
51
+ createBy: createBy,
52
+ carryForward: undefined,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Every role key already in use in this project, for uniqueness checks.
58
+ *
59
+ * Read as root: a member who may create a role must get a key that does not
60
+ * collide with one they are not allowed to see, and a partial view of the
61
+ * project's keys would hand them a duplicate the database then rejects.
62
+ */
63
+ public async getKeysInProject(projectId: ObjectID): Promise<Set<string>> {
64
+ const existing: Array<Model> = await this.findBy({
65
+ query: {
66
+ projectId: projectId,
67
+ },
68
+ select: {
69
+ key: true,
70
+ },
71
+ skip: 0,
72
+ limit: LIMIT_MAX,
73
+ props: {
74
+ isRoot: true,
75
+ },
76
+ });
77
+
78
+ const keys: Set<string> = new Set<string>();
79
+
80
+ for (const role of existing) {
81
+ if (role.key) {
82
+ keys.add(role.key);
83
+ }
84
+ }
85
+
86
+ return keys;
87
+ }
88
+ }
89
+
90
+ export default new Service();
@@ -34,6 +34,7 @@ import IncidentStateService from "./IncidentStateService";
34
34
  import IncidentRoleService from "./IncidentRoleService";
35
35
  import MailService from "./MailService";
36
36
  import MonitorStatusService from "./MonitorStatusService";
37
+ import NetworkDeviceRoleService from "./NetworkDeviceRoleService";
37
38
  import NetworkSiteTypeService from "./NetworkSiteTypeService";
38
39
  import NotificationService from "./NotificationService";
39
40
  import PromoCodeService from "./PromoCodeService";
@@ -79,6 +80,11 @@ import IncidentRole from "../../Models/DatabaseModels/IncidentRole";
79
80
  import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
80
81
  import NetworkSiteType from "../../Models/DatabaseModels/NetworkSiteType";
81
82
  import DefaultNetworkSiteType from "../../Types/NetworkSite/DefaultNetworkSiteType";
83
+ import NetworkDeviceRole from "../../Models/DatabaseModels/NetworkDeviceRole";
84
+ import {
85
+ DEFAULT_NETWORK_DEVICE_ROLES,
86
+ DefaultNetworkDeviceRole,
87
+ } from "../../Types/NetworkDevice/DefaultNetworkDeviceRole";
82
88
  import Model from "../../Models/DatabaseModels/Project";
83
89
  import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
84
90
  import PromoCode from "../../Models/DatabaseModels/PromoCode";
@@ -1444,6 +1450,7 @@ These are no longer recorded against the project and have to be cancelled by han
1444
1450
  this.addDefaultAlertState(createdItem),
1445
1451
  this.addDefaultIncidentRoles(createdItem),
1446
1452
  this.addDefaultNetworkSiteTypes(createdItem),
1453
+ this.addDefaultNetworkDeviceRoles(createdItem),
1447
1454
  ]);
1448
1455
 
1449
1456
  if (createdItem.createdOwnerEmail) {
@@ -2007,6 +2014,84 @@ These are no longer recorded against the project and have to be cancelled by han
2007
2014
  return createdItem;
2008
2015
  }
2009
2016
 
2017
+ /**
2018
+ * Seeds the per-project network device role lookup table.
2019
+ *
2020
+ * Device roles used to be a fixed union with the label, the silhouette and
2021
+ * the "is this a core device?" flag hardcoded in three different modules.
2022
+ * They are now a configurable per-project table, so a project can rename
2023
+ * "Wireless AP" to "Access Point", draw firewalls as something else, or add
2024
+ * a role of its own - which means every project needs its own seeded copy of
2025
+ * the defaults.
2026
+ *
2027
+ * Public for the same reason addDefaultNetworkSiteTypes is: the
2028
+ * BackfillNetworkDeviceRoles data migration calls it to seed roles into
2029
+ * projects that existed before the table did, so the defaults are defined
2030
+ * once and cannot drift between project creation and the backfill.
2031
+ *
2032
+ * The key is passed explicitly rather than derived from the name. It has to
2033
+ * be exactly what the SNMP classifier emits - "wirelessAccessPoint", which
2034
+ * no derivation from "Wireless AP" would produce - or a classified access
2035
+ * point would match no row.
2036
+ */
2037
+ @CaptureSpan()
2038
+ public async addDefaultNetworkDeviceRoles(
2039
+ createdItem: Model,
2040
+ ): Promise<Model> {
2041
+ const projectId: ObjectID = createdItem.id!;
2042
+
2043
+ // Idempotent - see getExistingProjectScopedNames.
2044
+ const existingNames: Set<string | undefined> =
2045
+ await this.getExistingProjectScopedNames(
2046
+ NetworkDeviceRoleService,
2047
+ projectId,
2048
+ );
2049
+
2050
+ /*
2051
+ * Also guarded on the key, not just the name: a project that renamed the
2052
+ * seeded "Router" to "Edge Router" must not get a second row called
2053
+ * "Router" the next time this runs, and the key is the identity that
2054
+ * survives the rename.
2055
+ */
2056
+ const existingKeys: Set<string> =
2057
+ await NetworkDeviceRoleService.getKeysInProject(projectId);
2058
+
2059
+ for (
2060
+ let index: number = 0;
2061
+ index < DEFAULT_NETWORK_DEVICE_ROLES.length;
2062
+ index++
2063
+ ) {
2064
+ const defaultRole: DefaultNetworkDeviceRole =
2065
+ DEFAULT_NETWORK_DEVICE_ROLES[index]!;
2066
+
2067
+ if (
2068
+ existingNames.has(defaultRole.name) ||
2069
+ existingKeys.has(defaultRole.key)
2070
+ ) {
2071
+ continue;
2072
+ }
2073
+
2074
+ const networkDeviceRole: NetworkDeviceRole = new NetworkDeviceRole();
2075
+ networkDeviceRole.name = defaultRole.name;
2076
+ networkDeviceRole.key = defaultRole.key;
2077
+ networkDeviceRole.description = defaultRole.description;
2078
+ networkDeviceRole.topologyShape = defaultRole.topologyShape;
2079
+ networkDeviceRole.isCoreLayer = defaultRole.isCoreLayer;
2080
+ networkDeviceRole.isSnmpWalkable = defaultRole.isSnmpWalkable;
2081
+ networkDeviceRole.projectId = projectId;
2082
+ networkDeviceRole.order = index + 1;
2083
+
2084
+ await NetworkDeviceRoleService.create({
2085
+ data: networkDeviceRole,
2086
+ props: {
2087
+ isRoot: true,
2088
+ },
2089
+ });
2090
+ }
2091
+
2092
+ return createdItem;
2093
+ }
2094
+
2010
2095
  private async addDefaultProjectTeams(createdItem: Model): Promise<Model> {
2011
2096
  // add a team member.
2012
2097
 
@@ -800,12 +800,6 @@ export class Service extends DatabaseService<UserNotificationSetting> {
800
800
  userId: ObjectID,
801
801
  projectId: ObjectID,
802
802
  ): Promise<void> {
803
- await this.addNotificationSettingIfNotExists(
804
- userId,
805
- projectId,
806
- NotificationSettingEventType.SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION,
807
- );
808
-
809
803
  await this.addNotificationSettingIfNotExists(
810
804
  userId,
811
805
  projectId,
@@ -128,12 +128,26 @@ const SECOND_SWITCH_NODE: NetworkTopologyNode = {
128
128
  status: "up",
129
129
  };
130
130
 
131
+ /*
132
+ * Carries the role stamp the topology builder puts on a node now that roles
133
+ * are per-project rows: `roleId` is the project's row for the classified role,
134
+ * and it is what the form can actually preselect - the role is a relation, and
135
+ * a form cannot resolve a key to a row.
136
+ */
137
+ const PHONE_ROLE_ID: string = "b7a1a0f6-3a4b-4a0e-9c3f-4f1f7f2a91cd";
138
+
139
+ const SWITCH_ROLE_ID: string = "0c2d5b41-9f22-4a3e-8f61-2d7c6a4e8b03";
140
+
131
141
  const PHONE_NODE: NetworkTopologyNode = {
132
142
  id: "unmanaged:sep6026aaf2b46b",
133
143
  name: "SEP6026AAF2B46B",
134
144
  isManaged: false,
135
145
  kind: "unmanaged",
136
146
  role: "phone",
147
+ roleKey: "phone",
148
+ roleLabel: "IP phone",
149
+ roleId: PHONE_ROLE_ID,
150
+ isSnmpWalkableRole: false,
137
151
  status: "unknown",
138
152
  deviceModel: "Cisco IP Phone 8811",
139
153
  ipAddress: "10.0.12.41",
@@ -227,7 +241,12 @@ describe("the Add to Monitoring dialog", () => {
227
241
  test("opens on the role the map classified the device as", async () => {
228
242
  const props: CapturedModalProps = await openDialog();
229
243
 
230
- expect(props.initialValues["deviceRole"]).toBe("phone");
244
+ /*
245
+ * The row's id, not the classifier's key: the role is a relation to the
246
+ * project's own NetworkDeviceRole table, so this is what the picker binds
247
+ * to and what a rename or a custom role travels through.
248
+ */
249
+ expect(props.initialValues["networkDeviceRole"]).toBe(PHONE_ROLE_ID);
231
250
  });
232
251
 
233
252
  /*
@@ -297,7 +316,18 @@ describe("the Add to Monitoring dialog", () => {
297
316
  test("inherits a probe for a device that will be polled", async () => {
298
317
  const props: CapturedModalProps = await openDialog({
299
318
  ...PHONE_NODE,
319
+ /*
320
+ * The whole role stamp, not just `role`: the configured
321
+ * isSnmpWalkableRole is what decides the monitoring method now, and the
322
+ * phone fixture carries the handset's (false). Overriding only the
323
+ * built-in role would leave the flag saying "nothing will poll this",
324
+ * which is the opposite of what this test is about.
325
+ */
300
326
  role: "switch",
327
+ roleKey: "switch",
328
+ roleLabel: "Switch",
329
+ roleId: SWITCH_ROLE_ID,
330
+ isSnmpWalkableRole: true,
301
331
  });
302
332
 
303
333
  expect(props.initialValues["monitoringMethod"]).toBe("SNMP");
@@ -20,13 +20,23 @@ import * as React from "react";
20
20
  import getJestMockFunction, { MockFunction } from "../../MockType";
21
21
 
22
22
  /*
23
- * Drag-to-zoom on a dashboard chart widget is the "investigate this
24
- * spike" entry point: selecting a window must narrow THIS widget only
25
- * (never the dashboard-wide range), refetch for the narrowed window, and
26
- * offer a way back (Reset) and a way deeper (Open in Explorer, carrying
27
- * the zoomed window). Edit mode must not zoom the drag gesture belongs
28
- * to widget move/resize there, and series pivots would navigate away from
29
- * unsaved dashboard changes.
23
+ * The chart widget's STANDALONE fallback: what drag-to-zoom does when the
24
+ * host hands down no onDashboardTimeRangeSelect the edit-mode settings
25
+ * preview, and any future surface that mounts the widget without owning a
26
+ * time range. Selecting a window narrows THIS widget, refetches for the
27
+ * narrowed window, and offers a way back (Reset) and a way deeper (Open in
28
+ * Explorer, carrying the zoomed window).
29
+ *
30
+ * On a real dashboard the shell DOES take the drag and the whole board
31
+ * retimes (issue #3530) — that contract lives in
32
+ * DashboardWideChartZoom.test.tsx. Both paths are pinned because the
33
+ * widget picks between them purely on whether the callback prop is
34
+ * present, and a host that forgets to pass it should degrade to this
35
+ * rather than lose the gesture.
36
+ *
37
+ * Edit mode must not zoom either way — the drag gesture belongs to widget
38
+ * move/resize there, and series pivots would navigate away from unsaved
39
+ * dashboard changes.
30
40
  */
31
41
 
32
42
  const fetchResultsMock: MockFunction = getJestMockFunction();
@@ -215,7 +225,24 @@ afterEach(() => {
215
225
  cleanup();
216
226
  });
217
227
 
218
- describe("dashboard chart widget drag-to-zoom", () => {
228
+ describe("dashboard chart widget drag-to-zoom (standalone fallback)", () => {
229
+ test("the fallback is what runs when the host offers no dashboard-wide zoom", async () => {
230
+ renderWidget();
231
+
232
+ await waitFor(() => {
233
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
234
+ });
235
+
236
+ /*
237
+ * Guards the branch this whole suite depends on: these props are the
238
+ * switch between narrowing this widget and retiming the board, and
239
+ * buildBaseProps deliberately leaves them off.
240
+ */
241
+ const baseProps: DashboardBaseComponentProps = buildBaseProps();
242
+ expect(baseProps.onDashboardTimeRangeSelect).toBeUndefined();
243
+ expect(baseProps.onDashboardTimeRangeReset).toBeUndefined();
244
+ });
245
+
219
246
  test("drag-selecting a window refetches THIS widget for that window and shows the zoom bar", async () => {
220
247
  renderWidget();
221
248