@oneuptime/common 10.0.34 → 10.0.35
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/Index.ts +3 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +640 -0
- package/Server/API/IPWhitelistAPI.ts +31 -0
- package/Server/API/Index.ts +2 -0
- package/Server/EnvironmentConfig.ts +2 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773761409952-MigrationName.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1774000000000-MigrationName.ts +80 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/KubernetesClusterService.ts +109 -0
- package/Server/Services/UserService.ts +6 -0
- package/Server/Services/UserSessionService.ts +23 -0
- package/Server/Utils/Express.ts +0 -6
- package/Server/Utils/StartServer.ts +7 -3
- package/Server/Utils/VM/VMRunner.ts +3 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/Permission.ts +42 -0
- package/UI/Components/Icon/Icon.tsx +51 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +2 -0
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +25 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +85 -53
- package/UI/Components/Navbar/NavBarMenu.tsx +2 -2
- package/Utils/Traces/CriticalPath.ts +348 -0
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +661 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -0
- package/build/dist/Server/API/IPWhitelistAPI.js +24 -0
- package/build/dist/Server/API/IPWhitelistAPI.js.map +1 -0
- package/build/dist/Server/API/Index.js +2 -0
- package/build/dist/Server/API/Index.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +1 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773761409952-MigrationName.js +52 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773761409952-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000000-MigrationName.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000000-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +117 -0
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +5 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSessionService.js +20 -0
- package/build/dist/Server/Services/UserSessionService.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +1 -42
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +4 -3
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +3 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Permission.js +36 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +27 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +4 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +50 -43
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenu.js +2 -2
- package/build/dist/Utils/Traces/CriticalPath.js +240 -0
- package/build/dist/Utils/Traces/CriticalPath.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1773761409952 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1773761409952";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
|
|
9
|
+
);
|
|
10
|
+
await queryRunner.query(
|
|
11
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
|
|
12
|
+
);
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
|
|
15
|
+
);
|
|
16
|
+
await queryRunner.query(
|
|
17
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
|
|
18
|
+
);
|
|
19
|
+
await queryRunner.query(
|
|
20
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
|
|
21
|
+
);
|
|
22
|
+
await queryRunner.query(
|
|
23
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
|
|
24
|
+
);
|
|
25
|
+
await queryRunner.query(
|
|
26
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
|
|
27
|
+
);
|
|
28
|
+
await queryRunner.query(
|
|
29
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
|
|
30
|
+
);
|
|
31
|
+
await queryRunner.query(
|
|
32
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
|
|
33
|
+
);
|
|
34
|
+
await queryRunner.query(
|
|
35
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
|
|
36
|
+
);
|
|
37
|
+
await queryRunner.query(
|
|
38
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
|
39
|
+
);
|
|
40
|
+
await queryRunner.query(
|
|
41
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
|
42
|
+
);
|
|
43
|
+
await queryRunner.query(
|
|
44
|
+
`CREATE INDEX "IDX_5ae5bbb0c93c048b0b76b1d426" ON "KubernetesCluster" ("projectId") `,
|
|
45
|
+
);
|
|
46
|
+
await queryRunner.query(
|
|
47
|
+
`CREATE INDEX "IDX_b9259f6741a7965a518e258f61" ON "KubernetesCluster" ("clusterIdentifier") `,
|
|
48
|
+
);
|
|
49
|
+
await queryRunner.query(
|
|
50
|
+
`CREATE INDEX "IDX_ed1b53bd041aa21b44ca8cdab5" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
|
|
51
|
+
);
|
|
52
|
+
await queryRunner.query(
|
|
53
|
+
`CREATE INDEX "IDX_2ec82ad068e84cf762c32ad7c7" ON "KubernetesClusterLabel" ("labelId") `,
|
|
54
|
+
);
|
|
55
|
+
await queryRunner.query(
|
|
56
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
57
|
+
);
|
|
58
|
+
await queryRunner.query(
|
|
59
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_1bee392c44b1aebe754932133a8" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
60
|
+
);
|
|
61
|
+
await queryRunner.query(
|
|
62
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
63
|
+
);
|
|
64
|
+
await queryRunner.query(
|
|
65
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
66
|
+
);
|
|
67
|
+
await queryRunner.query(
|
|
68
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
73
|
+
await queryRunner.query(
|
|
74
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_2ec82ad068e84cf762c32ad7c76"`,
|
|
75
|
+
);
|
|
76
|
+
await queryRunner.query(
|
|
77
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_ed1b53bd041aa21b44ca8cdab5e"`,
|
|
78
|
+
);
|
|
79
|
+
await queryRunner.query(
|
|
80
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_b0f6c98aac521060f8b68fe5c87"`,
|
|
81
|
+
);
|
|
82
|
+
await queryRunner.query(
|
|
83
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_1bee392c44b1aebe754932133a8"`,
|
|
84
|
+
);
|
|
85
|
+
await queryRunner.query(
|
|
86
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_5ae5bbb0c93c048b0b76b1d4268"`,
|
|
87
|
+
);
|
|
88
|
+
await queryRunner.query(
|
|
89
|
+
`DROP INDEX "public"."IDX_2ec82ad068e84cf762c32ad7c7"`,
|
|
90
|
+
);
|
|
91
|
+
await queryRunner.query(
|
|
92
|
+
`DROP INDEX "public"."IDX_ed1b53bd041aa21b44ca8cdab5"`,
|
|
93
|
+
);
|
|
94
|
+
await queryRunner.query(
|
|
95
|
+
`DROP INDEX "public"."IDX_b9259f6741a7965a518e258f61"`,
|
|
96
|
+
);
|
|
97
|
+
await queryRunner.query(
|
|
98
|
+
`DROP INDEX "public"."IDX_5ae5bbb0c93c048b0b76b1d426"`,
|
|
99
|
+
);
|
|
100
|
+
await queryRunner.query(
|
|
101
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
|
102
|
+
);
|
|
103
|
+
await queryRunner.query(
|
|
104
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
|
105
|
+
);
|
|
106
|
+
await queryRunner.query(
|
|
107
|
+
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId") `,
|
|
108
|
+
);
|
|
109
|
+
await queryRunner.query(
|
|
110
|
+
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId") `,
|
|
111
|
+
);
|
|
112
|
+
await queryRunner.query(
|
|
113
|
+
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug") `,
|
|
114
|
+
);
|
|
115
|
+
await queryRunner.query(
|
|
116
|
+
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier") `,
|
|
117
|
+
);
|
|
118
|
+
await queryRunner.query(
|
|
119
|
+
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId") `,
|
|
120
|
+
);
|
|
121
|
+
await queryRunner.query(
|
|
122
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
123
|
+
);
|
|
124
|
+
await queryRunner.query(
|
|
125
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
126
|
+
);
|
|
127
|
+
await queryRunner.query(
|
|
128
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
129
|
+
);
|
|
130
|
+
await queryRunner.query(
|
|
131
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
132
|
+
);
|
|
133
|
+
await queryRunner.query(
|
|
134
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1774000000000 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1774000000000";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`CREATE TABLE "KubernetesCluster" ("_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, "description" character varying(500), "clusterIdentifier" character varying(100) NOT NULL, "provider" character varying(100) DEFAULT 'unknown', "otelCollectorStatus" character varying(100) DEFAULT 'disconnected', "lastSeenAt" TIMESTAMP WITH TIME ZONE, "nodeCount" integer DEFAULT '0', "podCount" integer DEFAULT '0', "namespaceCount" integer DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_kubernetes_cluster_id" PRIMARY KEY ("_id"))`,
|
|
9
|
+
);
|
|
10
|
+
await queryRunner.query(
|
|
11
|
+
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId")`,
|
|
12
|
+
);
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier")`,
|
|
15
|
+
);
|
|
16
|
+
await queryRunner.query(
|
|
17
|
+
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug")`,
|
|
18
|
+
);
|
|
19
|
+
await queryRunner.query(
|
|
20
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
21
|
+
);
|
|
22
|
+
await queryRunner.query(
|
|
23
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
24
|
+
);
|
|
25
|
+
await queryRunner.query(
|
|
26
|
+
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
27
|
+
);
|
|
28
|
+
// Label join table
|
|
29
|
+
await queryRunner.query(
|
|
30
|
+
`CREATE TABLE "KubernetesClusterLabel" ("kubernetesClusterId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_kubernetes_cluster_label" PRIMARY KEY ("kubernetesClusterId", "labelId"))`,
|
|
31
|
+
);
|
|
32
|
+
await queryRunner.query(
|
|
33
|
+
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId")`,
|
|
34
|
+
);
|
|
35
|
+
await queryRunner.query(
|
|
36
|
+
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId")`,
|
|
37
|
+
);
|
|
38
|
+
await queryRunner.query(
|
|
39
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
40
|
+
);
|
|
41
|
+
await queryRunner.query(
|
|
42
|
+
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
47
|
+
await queryRunner.query(
|
|
48
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
|
|
49
|
+
);
|
|
50
|
+
await queryRunner.query(
|
|
51
|
+
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
|
|
52
|
+
);
|
|
53
|
+
await queryRunner.query(
|
|
54
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
|
|
55
|
+
);
|
|
56
|
+
await queryRunner.query(
|
|
57
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
|
|
58
|
+
);
|
|
59
|
+
await queryRunner.query(`DROP TABLE "KubernetesClusterLabel"`);
|
|
60
|
+
await queryRunner.query(
|
|
61
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
|
|
62
|
+
);
|
|
63
|
+
await queryRunner.query(
|
|
64
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
|
|
65
|
+
);
|
|
66
|
+
await queryRunner.query(
|
|
67
|
+
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
|
|
68
|
+
);
|
|
69
|
+
await queryRunner.query(
|
|
70
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
|
|
71
|
+
);
|
|
72
|
+
await queryRunner.query(
|
|
73
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
|
|
74
|
+
);
|
|
75
|
+
await queryRunner.query(
|
|
76
|
+
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
|
|
77
|
+
);
|
|
78
|
+
await queryRunner.query(`DROP TABLE "KubernetesCluster"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -266,6 +266,8 @@ import { AddLogSavedView1772355000000 } from "./1772355000000-AddLogSavedView";
|
|
|
266
266
|
import { MigrationName1773344537755 } from "./1773344537755-MigrationName";
|
|
267
267
|
import { MigrationName1773402621107 } from "./1773402621107-MigrationName";
|
|
268
268
|
import { MigrationName1773676206197 } from "./1773676206197-MigrationName";
|
|
269
|
+
import { MigrationName1774000000000 } from "./1774000000000-MigrationName";
|
|
270
|
+
import { MigrationName1773761409952 } from "./1773761409952-MigrationName";
|
|
269
271
|
|
|
270
272
|
export default [
|
|
271
273
|
InitialMigration,
|
|
@@ -536,4 +538,6 @@ export default [
|
|
|
536
538
|
MigrationName1773344537755,
|
|
537
539
|
MigrationName1773402621107,
|
|
538
540
|
MigrationName1773676206197,
|
|
541
|
+
MigrationName1774000000000,
|
|
542
|
+
MigrationName1773761409952,
|
|
539
543
|
];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/KubernetesCluster";
|
|
3
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
|
+
import ObjectID from "../../Types/ObjectID";
|
|
5
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
6
|
+
import OneUptimeDate from "../../Types/Date";
|
|
7
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
|
+
|
|
9
|
+
export class Service extends DatabaseService<Model> {
|
|
10
|
+
public constructor() {
|
|
11
|
+
super(Model);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@CaptureSpan()
|
|
15
|
+
public async findOrCreateByClusterIdentifier(data: {
|
|
16
|
+
projectId: ObjectID;
|
|
17
|
+
clusterIdentifier: string;
|
|
18
|
+
}): Promise<Model> {
|
|
19
|
+
// Try to find existing cluster
|
|
20
|
+
const existingCluster: Model | null = await this.findOneBy({
|
|
21
|
+
query: {
|
|
22
|
+
projectId: data.projectId,
|
|
23
|
+
clusterIdentifier: data.clusterIdentifier,
|
|
24
|
+
},
|
|
25
|
+
select: {
|
|
26
|
+
_id: true,
|
|
27
|
+
projectId: true,
|
|
28
|
+
clusterIdentifier: true,
|
|
29
|
+
},
|
|
30
|
+
props: {
|
|
31
|
+
isRoot: true,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (existingCluster) {
|
|
36
|
+
return existingCluster;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create new cluster
|
|
40
|
+
const newCluster: Model = new Model();
|
|
41
|
+
newCluster.projectId = data.projectId;
|
|
42
|
+
newCluster.name = data.clusterIdentifier;
|
|
43
|
+
newCluster.clusterIdentifier = data.clusterIdentifier;
|
|
44
|
+
newCluster.otelCollectorStatus = "connected";
|
|
45
|
+
newCluster.lastSeenAt = OneUptimeDate.getCurrentDate();
|
|
46
|
+
|
|
47
|
+
const createdCluster: Model = await this.create({
|
|
48
|
+
data: newCluster,
|
|
49
|
+
props: {
|
|
50
|
+
isRoot: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return createdCluster;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@CaptureSpan()
|
|
58
|
+
public async updateLastSeen(clusterId: ObjectID): Promise<void> {
|
|
59
|
+
await this.updateOneById({
|
|
60
|
+
id: clusterId,
|
|
61
|
+
data: {
|
|
62
|
+
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
63
|
+
otelCollectorStatus: "connected",
|
|
64
|
+
},
|
|
65
|
+
props: {
|
|
66
|
+
isRoot: true,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@CaptureSpan()
|
|
72
|
+
public async markDisconnectedClusters(): Promise<void> {
|
|
73
|
+
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
|
74
|
+
OneUptimeDate.getCurrentDate(),
|
|
75
|
+
-5,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const connectedClusters: Array<Model> = await this.findBy({
|
|
79
|
+
query: {
|
|
80
|
+
otelCollectorStatus: "connected",
|
|
81
|
+
lastSeenAt: QueryHelper.lessThan(fiveMinutesAgo),
|
|
82
|
+
},
|
|
83
|
+
select: {
|
|
84
|
+
_id: true,
|
|
85
|
+
},
|
|
86
|
+
limit: LIMIT_MAX,
|
|
87
|
+
skip: 0,
|
|
88
|
+
props: {
|
|
89
|
+
isRoot: true,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
for (const cluster of connectedClusters) {
|
|
94
|
+
if (cluster._id) {
|
|
95
|
+
await this.updateOneById({
|
|
96
|
+
id: new ObjectID(cluster._id.toString()),
|
|
97
|
+
data: {
|
|
98
|
+
otelCollectorStatus: "disconnected",
|
|
99
|
+
},
|
|
100
|
+
props: {
|
|
101
|
+
isRoot: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default new Service();
|
|
@@ -13,6 +13,7 @@ import MailService from "./MailService";
|
|
|
13
13
|
import TeamMemberService from "./TeamMemberService";
|
|
14
14
|
import UserNotificationRuleService from "./UserNotificationRuleService";
|
|
15
15
|
import UserNotificationSettingService from "./UserNotificationSettingService";
|
|
16
|
+
import UserSessionService from "./UserSessionService";
|
|
16
17
|
import { AccountsRoute } from "../../ServiceRoute";
|
|
17
18
|
import Hostname from "../../Types/API/Hostname";
|
|
18
19
|
import Protocol from "../../Types/API/Protocol";
|
|
@@ -252,6 +253,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
252
253
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
253
254
|
|
|
254
255
|
for (const user of onUpdate.carryForward) {
|
|
256
|
+
// Revoke all active sessions for this user on password change
|
|
257
|
+
await UserSessionService.revokeAllSessionsByUserId(user.id!, {
|
|
258
|
+
reason: "Password changed",
|
|
259
|
+
});
|
|
260
|
+
|
|
255
261
|
// password changed, send password changed mail
|
|
256
262
|
MailService.sendMail({
|
|
257
263
|
toEmail: user.email!,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/UserSession";
|
|
3
3
|
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
4
5
|
import { JSONObject } from "../../Types/JSON";
|
|
5
6
|
import HashedString from "../../Types/HashedString";
|
|
6
7
|
import { EncryptionSecret } from "../EnvironmentConfig";
|
|
@@ -275,6 +276,28 @@ export class Service extends DatabaseService<Model> {
|
|
|
275
276
|
await this.revokeSessionById(session.id, options);
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
public async revokeAllSessionsByUserId(
|
|
280
|
+
userId: ObjectID,
|
|
281
|
+
options?: RevokeSessionOptions,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
await this.updateBy({
|
|
284
|
+
query: {
|
|
285
|
+
userId: userId,
|
|
286
|
+
isRevoked: false,
|
|
287
|
+
},
|
|
288
|
+
data: {
|
|
289
|
+
isRevoked: true,
|
|
290
|
+
revokedAt: OneUptimeDate.getCurrentDate(),
|
|
291
|
+
revokedReason: options?.reason ?? null,
|
|
292
|
+
},
|
|
293
|
+
limit: LIMIT_MAX,
|
|
294
|
+
skip: 0,
|
|
295
|
+
props: {
|
|
296
|
+
isRoot: true,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
278
301
|
private buildSessionModel(
|
|
279
302
|
options: CreateSessionOptions,
|
|
280
303
|
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
package/Server/Utils/Express.ts
CHANGED
|
@@ -13,7 +13,6 @@ import UserType from "../../Types/UserType";
|
|
|
13
13
|
import "ejs";
|
|
14
14
|
import express from "express";
|
|
15
15
|
import { Server, createServer } from "http";
|
|
16
|
-
import CaptureSpan from "./Telemetry/CaptureSpan";
|
|
17
16
|
|
|
18
17
|
export type RequestHandler = express.RequestHandler;
|
|
19
18
|
export type NextFunction = express.NextFunction;
|
|
@@ -61,22 +60,18 @@ class Express {
|
|
|
61
60
|
private static app: express.Application;
|
|
62
61
|
private static httpServer: Server;
|
|
63
62
|
|
|
64
|
-
@CaptureSpan()
|
|
65
63
|
public static getRouter(): express.Router {
|
|
66
64
|
return express.Router();
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
@CaptureSpan()
|
|
70
67
|
public static setupExpress(): void {
|
|
71
68
|
this.app = express();
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
@CaptureSpan()
|
|
75
71
|
public static getHttpServer(): Server {
|
|
76
72
|
return this.httpServer;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
@CaptureSpan()
|
|
80
75
|
public static getExpressApp(): express.Application {
|
|
81
76
|
if (!this.app) {
|
|
82
77
|
this.setupExpress();
|
|
@@ -85,7 +80,6 @@ class Express {
|
|
|
85
80
|
return this.app;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
@CaptureSpan()
|
|
89
83
|
public static async launchApplication(
|
|
90
84
|
appName: string,
|
|
91
85
|
port?: Port,
|
|
@@ -37,6 +37,7 @@ import Typeof from "../../Types/Typeof";
|
|
|
37
37
|
import CookieParser from "cookie-parser";
|
|
38
38
|
import cors from "cors";
|
|
39
39
|
import zlib from "zlib";
|
|
40
|
+
import path from "path";
|
|
40
41
|
import "ejs";
|
|
41
42
|
// Make sure we have stack trace for debugging.
|
|
42
43
|
Error.stackTraceLimit = Infinity;
|
|
@@ -240,12 +241,15 @@ const init: InitFunction = async (
|
|
|
240
241
|
},
|
|
241
242
|
);
|
|
242
243
|
|
|
243
|
-
app.use(
|
|
244
|
+
app.use(
|
|
245
|
+
`/${appName}`,
|
|
246
|
+
ExpressStatic(path.resolve(process.cwd(), "public")),
|
|
247
|
+
);
|
|
244
248
|
|
|
245
249
|
app.get(
|
|
246
250
|
`/${appName}/dist/Index.js`,
|
|
247
251
|
(_req: ExpressRequest, res: ExpressResponse) => {
|
|
248
|
-
res.sendFile("
|
|
252
|
+
res.sendFile(path.resolve(process.cwd(), "public/dist/Index.js"));
|
|
249
253
|
},
|
|
250
254
|
);
|
|
251
255
|
|
|
@@ -285,7 +289,7 @@ const init: InitFunction = async (
|
|
|
285
289
|
return;
|
|
286
290
|
}
|
|
287
291
|
|
|
288
|
-
return res.render("
|
|
292
|
+
return res.render(path.resolve(process.cwd(), "views/index.ejs"), {
|
|
289
293
|
enableGoogleTagManager: IsBillingEnabled || false,
|
|
290
294
|
...variables,
|
|
291
295
|
});
|
|
@@ -28,9 +28,12 @@ const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
|
|
|
28
28
|
* and traversal via page.context().browser().browserType().launch(...)
|
|
29
29
|
*/
|
|
30
30
|
"browserType", // Browser → BrowserType (which has launch/connect)
|
|
31
|
+
"_browserType", // Internal alias for browserType — same escape vector
|
|
31
32
|
"launch", // BrowserType.launch() spawns a child process
|
|
33
|
+
"launchServer", // BrowserType.launchServer() spawns a browser server process
|
|
32
34
|
"launchPersistentContext", // BrowserType.launchPersistentContext() spawns a child process
|
|
33
35
|
"connectOverCDP", // BrowserType.connectOverCDP() connects via Chrome DevTools Protocol
|
|
36
|
+
"connect", // BrowserType.connect() connects to a remote browser
|
|
34
37
|
"newCDPSession", // BrowserContext/Page.newCDPSession() opens raw CDP sessions
|
|
35
38
|
]);
|
|
36
39
|
|
package/Types/Icon/IconProp.ts
CHANGED
package/Types/Permission.ts
CHANGED
|
@@ -705,6 +705,11 @@ enum Permission {
|
|
|
705
705
|
DeleteAlertSeverity = "DeleteAlertSeverity",
|
|
706
706
|
ReadAlertSeverity = "ReadAlertSeverity",
|
|
707
707
|
|
|
708
|
+
CreateKubernetesCluster = "CreateKubernetesCluster",
|
|
709
|
+
DeleteKubernetesCluster = "DeleteKubernetesCluster",
|
|
710
|
+
EditKubernetesCluster = "EditKubernetesCluster",
|
|
711
|
+
ReadKubernetesCluster = "ReadKubernetesCluster",
|
|
712
|
+
|
|
708
713
|
CreateService = "CreateService",
|
|
709
714
|
DeleteService = "DeleteService",
|
|
710
715
|
EditService = "EditService",
|
|
@@ -4324,6 +4329,43 @@ export class PermissionHelper {
|
|
|
4324
4329
|
group: PermissionGroup.AIAgent,
|
|
4325
4330
|
},
|
|
4326
4331
|
|
|
4332
|
+
{
|
|
4333
|
+
permission: Permission.CreateKubernetesCluster,
|
|
4334
|
+
title: "Create Kubernetes Cluster",
|
|
4335
|
+
description:
|
|
4336
|
+
"This permission can create Kubernetes Cluster in this project.",
|
|
4337
|
+
isAssignableToTenant: true,
|
|
4338
|
+
isAccessControlPermission: true,
|
|
4339
|
+
group: PermissionGroup.Telemetry,
|
|
4340
|
+
},
|
|
4341
|
+
{
|
|
4342
|
+
permission: Permission.DeleteKubernetesCluster,
|
|
4343
|
+
title: "Delete Kubernetes Cluster",
|
|
4344
|
+
description:
|
|
4345
|
+
"This permission can delete Kubernetes Cluster of this project.",
|
|
4346
|
+
isAssignableToTenant: true,
|
|
4347
|
+
isAccessControlPermission: false,
|
|
4348
|
+
group: PermissionGroup.Telemetry,
|
|
4349
|
+
},
|
|
4350
|
+
{
|
|
4351
|
+
permission: Permission.EditKubernetesCluster,
|
|
4352
|
+
title: "Edit Kubernetes Cluster",
|
|
4353
|
+
description:
|
|
4354
|
+
"This permission can edit Kubernetes Cluster of this project.",
|
|
4355
|
+
isAssignableToTenant: true,
|
|
4356
|
+
isAccessControlPermission: false,
|
|
4357
|
+
group: PermissionGroup.Telemetry,
|
|
4358
|
+
},
|
|
4359
|
+
{
|
|
4360
|
+
permission: Permission.ReadKubernetesCluster,
|
|
4361
|
+
title: "Read Kubernetes Cluster",
|
|
4362
|
+
description:
|
|
4363
|
+
"This permission can read Kubernetes Cluster of this project.",
|
|
4364
|
+
isAssignableToTenant: true,
|
|
4365
|
+
isAccessControlPermission: false,
|
|
4366
|
+
group: PermissionGroup.Telemetry,
|
|
4367
|
+
},
|
|
4368
|
+
|
|
4327
4369
|
{
|
|
4328
4370
|
permission: Permission.CreateService,
|
|
4329
4371
|
title: "Create Service",
|
|
@@ -2733,6 +2733,57 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
|
|
2733
2733
|
d="m15 11.25 1.5 1.5.75-.75V8.758l2.276-.61a3 3 0 1 0-3.675-3.675l-.61 2.277H12l-.75.75 1.5 1.5M15 11.25l-8.47 8.47c-.34.34-.8.53-1.28.53s-.94-.19-1.28-.53a1.818 1.818 0 0 1 0-2.56l8.47-8.47M15 11.25 12 8.25"
|
|
2734
2734
|
/>,
|
|
2735
2735
|
);
|
|
2736
|
+
} else if (icon === IconProp.Kubernetes) {
|
|
2737
|
+
// Kubernetes helm wheel — 7-sided shape with 7 spokes, matching the official logo
|
|
2738
|
+
const cx: number = 12;
|
|
2739
|
+
const cy: number = 12;
|
|
2740
|
+
const outerR: number = 9.5;
|
|
2741
|
+
const innerR: number = 2.2;
|
|
2742
|
+
const spokeEnd: number = 8;
|
|
2743
|
+
const sides: number = 7;
|
|
2744
|
+
const offsetAngle: number = -Math.PI / 2; // start from top
|
|
2745
|
+
|
|
2746
|
+
const outerPoints: string[] = [];
|
|
2747
|
+
const spokes: React.ReactElement[] = [];
|
|
2748
|
+
|
|
2749
|
+
for (let i: number = 0; i < sides; i++) {
|
|
2750
|
+
const angle: number = offsetAngle + (2 * Math.PI * i) / sides;
|
|
2751
|
+
const ox: number = cx + outerR * Math.cos(angle);
|
|
2752
|
+
const oy: number = cy + outerR * Math.sin(angle);
|
|
2753
|
+
outerPoints.push(`${ox.toFixed(2)},${oy.toFixed(2)}`);
|
|
2754
|
+
|
|
2755
|
+
const sx: number = cx + innerR * Math.cos(angle);
|
|
2756
|
+
const sy: number = cy + innerR * Math.sin(angle);
|
|
2757
|
+
const ex: number = cx + spokeEnd * Math.cos(angle);
|
|
2758
|
+
const ey: number = cy + spokeEnd * Math.sin(angle);
|
|
2759
|
+
|
|
2760
|
+
spokes.push(
|
|
2761
|
+
<line
|
|
2762
|
+
key={`spoke-${i}`}
|
|
2763
|
+
x1={sx.toFixed(2)}
|
|
2764
|
+
y1={sy.toFixed(2)}
|
|
2765
|
+
x2={ex.toFixed(2)}
|
|
2766
|
+
y2={ey.toFixed(2)}
|
|
2767
|
+
stroke="currentColor"
|
|
2768
|
+
strokeWidth="1.5"
|
|
2769
|
+
strokeLinecap="round"
|
|
2770
|
+
/>,
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
return getSvgWrapper(
|
|
2775
|
+
<>
|
|
2776
|
+
<polygon
|
|
2777
|
+
points={outerPoints.join(" ")}
|
|
2778
|
+
fill="none"
|
|
2779
|
+
stroke="currentColor"
|
|
2780
|
+
strokeWidth="1.5"
|
|
2781
|
+
strokeLinejoin="round"
|
|
2782
|
+
/>
|
|
2783
|
+
<circle cx={cx} cy={cy} r={innerR} />
|
|
2784
|
+
{spokes}
|
|
2785
|
+
</>,
|
|
2786
|
+
);
|
|
2736
2787
|
}
|
|
2737
2788
|
|
|
2738
2789
|
return <></>;
|
|
@@ -102,6 +102,7 @@ export interface ComponentProps {
|
|
|
102
102
|
onEditSavedView?: ((viewId: string) => void) | undefined;
|
|
103
103
|
onDeleteSavedView?: ((viewId: string) => void) | undefined;
|
|
104
104
|
onUpdateCurrentSavedView?: (() => void) | undefined;
|
|
105
|
+
onShowDocumentation?: (() => void) | undefined;
|
|
105
106
|
viewMode?: LogsViewMode | undefined;
|
|
106
107
|
onViewModeChange?: ((mode: LogsViewMode) => void) | undefined;
|
|
107
108
|
analyticsServiceIds?: Array<string> | undefined;
|
|
@@ -805,6 +806,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
805
806
|
return !prev;
|
|
806
807
|
});
|
|
807
808
|
},
|
|
809
|
+
onShowDocumentation: props.onShowDocumentation,
|
|
808
810
|
};
|
|
809
811
|
|
|
810
812
|
const showSidebar: boolean =
|
|
@@ -37,6 +37,7 @@ export interface LogsViewerToolbarProps {
|
|
|
37
37
|
onExportJSON?: (() => void) | undefined;
|
|
38
38
|
showKeyboardShortcuts?: boolean | undefined;
|
|
39
39
|
onToggleKeyboardShortcuts?: (() => void) | undefined;
|
|
40
|
+
onShowDocumentation?: (() => void) | undefined;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
|
@@ -155,6 +156,30 @@ const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
|
|
155
156
|
/>
|
|
156
157
|
)}
|
|
157
158
|
|
|
159
|
+
{props.onShowDocumentation && (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50"
|
|
163
|
+
onClick={props.onShowDocumentation}
|
|
164
|
+
title="Setup Documentation"
|
|
165
|
+
>
|
|
166
|
+
<svg
|
|
167
|
+
className="h-3.5 w-3.5"
|
|
168
|
+
fill="none"
|
|
169
|
+
viewBox="0 0 24 24"
|
|
170
|
+
strokeWidth={1.5}
|
|
171
|
+
stroke="currentColor"
|
|
172
|
+
>
|
|
173
|
+
<path
|
|
174
|
+
strokeLinecap="round"
|
|
175
|
+
strokeLinejoin="round"
|
|
176
|
+
d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
|
|
177
|
+
/>
|
|
178
|
+
</svg>
|
|
179
|
+
Docs
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
|
|
158
183
|
{props.onToggleKeyboardShortcuts && (
|
|
159
184
|
<div className="relative">
|
|
160
185
|
<button
|