@oneuptime/common 9.3.5 → 9.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Server/API/AIAgentDataAPI.ts +11 -1
- package/Server/API/GitHubAPI.ts +102 -0
- package/Server/EnvironmentConfig.ts +9 -0
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +53 -1
- package/Types/Icon/IconProp.ts +3 -0
- package/UI/Components/Icon/Icon.tsx +32 -0
- package/UI/Components/Navbar/NavBar.tsx +5 -2
- package/UI/Components/Navbar/NavBarMenu.tsx +48 -14
- package/UI/Components/Navbar/NavBarMenuItem.tsx +104 -6
- package/build/dist/Server/API/AIAgentDataAPI.js +11 -2
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/GitHubAPI.js +76 -3
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +34 -3
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Types/Icon/IconProp.js +3 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +17 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenu.js +26 -10
- package/build/dist/UI/Components/Navbar/NavBarMenu.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuItem.js +90 -6
- package/build/dist/UI/Components/Navbar/NavBarMenuItem.js.map +1 -1
- package/package.json +1 -1
|
@@ -458,10 +458,20 @@ export default class AIAgentDataAPI {
|
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
|
|
461
|
+
/*
|
|
462
|
+
* Generate GitHub installation access token with write permissions
|
|
463
|
+
* Required for AI Agent to push branches and create pull requests
|
|
464
|
+
*/
|
|
462
465
|
const tokenData: GitHubInstallationToken =
|
|
463
466
|
await GitHubUtil.getInstallationAccessToken(
|
|
464
467
|
codeRepository.gitHubAppInstallationId,
|
|
468
|
+
{
|
|
469
|
+
permissions: {
|
|
470
|
+
contents: "write", // Required for pushing branches
|
|
471
|
+
pull_requests: "write", // Required for creating PRs
|
|
472
|
+
metadata: "read", // Required for reading repository metadata
|
|
473
|
+
},
|
|
474
|
+
},
|
|
465
475
|
);
|
|
466
476
|
|
|
467
477
|
const repositoryUrl: string = `https://github.com/${codeRepository.organizationName}/${codeRepository.repositoryName}.git`;
|
package/Server/API/GitHubAPI.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { DashboardClientUrl, GitHubAppName } from "../EnvironmentConfig";
|
|
|
11
11
|
import ObjectID from "../../Types/ObjectID";
|
|
12
12
|
import GitHubUtil, {
|
|
13
13
|
GitHubRepository,
|
|
14
|
+
GitHubInstallationNotFoundError,
|
|
14
15
|
} from "../Utils/CodeRepository/GitHub/GitHub";
|
|
15
16
|
import CodeRepositoryService from "../Services/CodeRepositoryService";
|
|
16
17
|
import ProjectService from "../Services/ProjectService";
|
|
@@ -181,9 +182,19 @@ export default class GitHubAPI {
|
|
|
181
182
|
UserMiddleware.getUserMiddleware,
|
|
182
183
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
183
184
|
try {
|
|
185
|
+
const projectId: string | undefined =
|
|
186
|
+
req.params["projectId"]?.toString();
|
|
184
187
|
const installationId: string | undefined =
|
|
185
188
|
req.params["installationId"]?.toString();
|
|
186
189
|
|
|
190
|
+
if (!projectId) {
|
|
191
|
+
return Response.sendErrorResponse(
|
|
192
|
+
req,
|
|
193
|
+
res,
|
|
194
|
+
new BadDataException("Project ID is required"),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
187
198
|
if (!installationId) {
|
|
188
199
|
return Response.sendErrorResponse(
|
|
189
200
|
req,
|
|
@@ -201,6 +212,40 @@ export default class GitHubAPI {
|
|
|
201
212
|
} catch (error) {
|
|
202
213
|
logger.error("GitHub List Repositories Error:");
|
|
203
214
|
logger.error(error);
|
|
215
|
+
|
|
216
|
+
// Handle stale installation ID - clear it from the project and return specific error
|
|
217
|
+
if (error instanceof GitHubInstallationNotFoundError) {
|
|
218
|
+
const projectId: string | undefined =
|
|
219
|
+
req.params["projectId"]?.toString();
|
|
220
|
+
|
|
221
|
+
if (projectId) {
|
|
222
|
+
try {
|
|
223
|
+
// Clear the stale installation ID from the project
|
|
224
|
+
await ProjectService.updateOneById({
|
|
225
|
+
id: new ObjectID(projectId),
|
|
226
|
+
data: {
|
|
227
|
+
gitHubAppInstallationId: null as unknown as string,
|
|
228
|
+
},
|
|
229
|
+
props: {
|
|
230
|
+
isRoot: true,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
logger.info(
|
|
235
|
+
`Cleared stale GitHub App installation ID from project ${projectId}`,
|
|
236
|
+
);
|
|
237
|
+
} catch (clearError) {
|
|
238
|
+
logger.error(
|
|
239
|
+
"Failed to clear stale installation ID from project:",
|
|
240
|
+
);
|
|
241
|
+
logger.error(clearError);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Return the specific error so the frontend knows to prompt reinstallation
|
|
246
|
+
return Response.sendErrorResponse(req, res, error);
|
|
247
|
+
}
|
|
248
|
+
|
|
204
249
|
return Response.sendErrorResponse(
|
|
205
250
|
req,
|
|
206
251
|
res,
|
|
@@ -349,6 +394,63 @@ export default class GitHubAPI {
|
|
|
349
394
|
|
|
350
395
|
logger.debug(`Received GitHub webhook event: ${event}`);
|
|
351
396
|
|
|
397
|
+
// Handle installation events - specifically when the app is uninstalled
|
|
398
|
+
if (event === "installation") {
|
|
399
|
+
const action: string | undefined = (req.body as JSONObject)?.[
|
|
400
|
+
"action"
|
|
401
|
+
]?.toString();
|
|
402
|
+
const installationId: string | undefined = (
|
|
403
|
+
(req.body as JSONObject)?.["installation"] as JSONObject
|
|
404
|
+
)?.["id"]?.toString();
|
|
405
|
+
|
|
406
|
+
if (action === "deleted" && installationId) {
|
|
407
|
+
logger.info(
|
|
408
|
+
`GitHub App installation ${installationId} was deleted. Clearing from database...`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
// Clear the installation ID from any projects that have it
|
|
413
|
+
await ProjectService.updateBy({
|
|
414
|
+
query: {
|
|
415
|
+
gitHubAppInstallationId: installationId,
|
|
416
|
+
},
|
|
417
|
+
data: {
|
|
418
|
+
gitHubAppInstallationId: null as unknown as string,
|
|
419
|
+
},
|
|
420
|
+
limit: 1000,
|
|
421
|
+
skip: 0,
|
|
422
|
+
props: {
|
|
423
|
+
isRoot: true,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Also clear from any code repositories that have this installation ID
|
|
428
|
+
await CodeRepositoryService.updateBy({
|
|
429
|
+
query: {
|
|
430
|
+
gitHubAppInstallationId: installationId,
|
|
431
|
+
},
|
|
432
|
+
data: {
|
|
433
|
+
gitHubAppInstallationId: null as unknown as string,
|
|
434
|
+
},
|
|
435
|
+
limit: 10000,
|
|
436
|
+
skip: 0,
|
|
437
|
+
props: {
|
|
438
|
+
isRoot: true,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
logger.info(
|
|
443
|
+
`Successfully cleared GitHub App installation ${installationId} from database`,
|
|
444
|
+
);
|
|
445
|
+
} catch (clearError) {
|
|
446
|
+
logger.error(
|
|
447
|
+
`Failed to clear GitHub App installation ${installationId} from database:`,
|
|
448
|
+
);
|
|
449
|
+
logger.error(clearError);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
352
454
|
/*
|
|
353
455
|
* Handle different webhook events here
|
|
354
456
|
* For now, just acknowledge receipt
|
|
@@ -397,6 +397,15 @@ export const StatusPageApiClientUrl: URL = new URL(
|
|
|
397
397
|
new Route(StatusPageApiRoute.toString()),
|
|
398
398
|
);
|
|
399
399
|
|
|
400
|
+
/*
|
|
401
|
+
*Internal URL for server-to-server communication (uses internal Docker hostname)
|
|
402
|
+
*Note: The internal path is /api/status-page (not /status-page-api) because
|
|
403
|
+
* /status-page-api is the external route that Nginx rewrites to /api/status-page
|
|
404
|
+
*/
|
|
405
|
+
export const StatusPageApiInternalUrl: URL = URL.fromString(
|
|
406
|
+
AppApiClientUrl.toString(),
|
|
407
|
+
).addRoute(new Route("/status-page"));
|
|
408
|
+
|
|
400
409
|
export const DashboardClientUrl: URL = new URL(
|
|
401
410
|
HttpProtocol,
|
|
402
411
|
Host,
|
|
@@ -18,6 +18,17 @@ import {
|
|
|
18
18
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
19
19
|
import * as crypto from "crypto";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when a GitHub App installation is no longer valid (e.g., uninstalled from GitHub)
|
|
23
|
+
*/
|
|
24
|
+
export class GitHubInstallationNotFoundError extends BadDataException {
|
|
25
|
+
public constructor() {
|
|
26
|
+
super(
|
|
27
|
+
"GitHub App installation not found. The app may have been uninstalled from GitHub. Please reconnect with GitHub to reinstall the app.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export interface GitHubRepository {
|
|
22
33
|
id: number;
|
|
23
34
|
name: string;
|
|
@@ -335,11 +346,20 @@ export default class GitHubUtil extends HostedCodeRepository {
|
|
|
335
346
|
/**
|
|
336
347
|
* Gets an installation access token for a GitHub App installation
|
|
337
348
|
* @param installationId - The GitHub App installation ID
|
|
349
|
+
* @param options - Optional configuration for the token
|
|
350
|
+
* @param options.permissions - Specific permissions to request for the token
|
|
338
351
|
* @returns Installation token and expiration date
|
|
339
352
|
*/
|
|
340
353
|
@CaptureSpan()
|
|
341
354
|
public static async getInstallationAccessToken(
|
|
342
355
|
installationId: string,
|
|
356
|
+
options?: {
|
|
357
|
+
permissions?: {
|
|
358
|
+
contents?: "read" | "write";
|
|
359
|
+
pull_requests?: "read" | "write";
|
|
360
|
+
metadata?: "read";
|
|
361
|
+
};
|
|
362
|
+
},
|
|
343
363
|
): Promise<GitHubInstallationToken> {
|
|
344
364
|
const jwt: string = GitHubUtil.generateAppJWT();
|
|
345
365
|
|
|
@@ -347,10 +367,17 @@ export default class GitHubUtil extends HostedCodeRepository {
|
|
|
347
367
|
`https://api.github.com/app/installations/${installationId}/access_tokens`,
|
|
348
368
|
);
|
|
349
369
|
|
|
370
|
+
// Build request data with optional permissions
|
|
371
|
+
const requestData: JSONObject = {};
|
|
372
|
+
|
|
373
|
+
if (options?.permissions) {
|
|
374
|
+
requestData["permissions"] = options.permissions;
|
|
375
|
+
}
|
|
376
|
+
|
|
350
377
|
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post(
|
|
351
378
|
{
|
|
352
379
|
url: url,
|
|
353
|
-
data:
|
|
380
|
+
data: requestData,
|
|
354
381
|
headers: {
|
|
355
382
|
Authorization: `Bearer ${jwt}`,
|
|
356
383
|
Accept: "application/vnd.github+json",
|
|
@@ -360,6 +387,31 @@ export default class GitHubUtil extends HostedCodeRepository {
|
|
|
360
387
|
);
|
|
361
388
|
|
|
362
389
|
if (result instanceof HTTPErrorResponse) {
|
|
390
|
+
// Check if this is a permission error and provide helpful message
|
|
391
|
+
const errorMessage: string =
|
|
392
|
+
(result.data as JSONObject)?.["message"]?.toString() || "";
|
|
393
|
+
|
|
394
|
+
// Check if the installation is not found (404) - this means the app was uninstalled from GitHub
|
|
395
|
+
if (result.statusCode === 404) {
|
|
396
|
+
logger.error(
|
|
397
|
+
`GitHub App installation not found (ID: ${installationId}). ` +
|
|
398
|
+
`The app may have been uninstalled from GitHub. User needs to reinstall the app.`,
|
|
399
|
+
);
|
|
400
|
+
throw new GitHubInstallationNotFoundError();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
errorMessage.includes("permissions") ||
|
|
405
|
+
result.statusCode === 403 ||
|
|
406
|
+
result.statusCode === 422
|
|
407
|
+
) {
|
|
408
|
+
logger.error(
|
|
409
|
+
`GitHub App permission error: ${errorMessage}. ` +
|
|
410
|
+
`Please ensure the GitHub App is configured with the required permissions ` +
|
|
411
|
+
`(contents: write, pull_requests: write, metadata: read) in the GitHub App settings.`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
363
415
|
throw result;
|
|
364
416
|
}
|
|
365
417
|
|
package/Types/Icon/IconProp.ts
CHANGED
|
@@ -692,6 +692,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
|
|
692
692
|
clipRule="evenodd"
|
|
693
693
|
/>,
|
|
694
694
|
);
|
|
695
|
+
} else if (icon === IconProp.GitHub) {
|
|
696
|
+
return getSvgWrapper(
|
|
697
|
+
<path
|
|
698
|
+
fill="currentColor"
|
|
699
|
+
stroke="none"
|
|
700
|
+
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
|
701
|
+
/>,
|
|
702
|
+
);
|
|
695
703
|
} else if (icon === IconProp.ChevronRight) {
|
|
696
704
|
return getSvgWrapper(
|
|
697
705
|
<path
|
|
@@ -1298,6 +1306,30 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
|
|
1298
1306
|
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
|
|
1299
1307
|
/>,
|
|
1300
1308
|
);
|
|
1309
|
+
} else if (icon === IconProp.FlowDiagram) {
|
|
1310
|
+
// Flow diagram icon matching home page workflows - two boxes at top, one at bottom, connected
|
|
1311
|
+
return getSvgWrapper(
|
|
1312
|
+
<>
|
|
1313
|
+
<rect x="3" y="3" width="6" height="4" rx="1" strokeWidth="1.5" />
|
|
1314
|
+
<rect x="15" y="3" width="6" height="4" rx="1" strokeWidth="1.5" />
|
|
1315
|
+
<rect x="9" y="17" width="6" height="4" rx="1" strokeWidth="1.5" />
|
|
1316
|
+
<path
|
|
1317
|
+
strokeLinecap="round"
|
|
1318
|
+
d="M6 7v3a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7M12 12v5"
|
|
1319
|
+
/>
|
|
1320
|
+
</>,
|
|
1321
|
+
);
|
|
1322
|
+
} else if (icon === IconProp.Bug) {
|
|
1323
|
+
// Bug icon for exceptions - matching home page
|
|
1324
|
+
return getSvgWrapper(
|
|
1325
|
+
<>
|
|
1326
|
+
<ellipse cx="12" cy="14" rx="5" ry="6" />
|
|
1327
|
+
<path
|
|
1328
|
+
strokeLinecap="round"
|
|
1329
|
+
d="M9 8.5C9 6.5 10.5 5 12 5s3 1.5 3 3.5M4 11l3 1m10-1 3 1M4 17l3-1m10 1 3-1M12 8v12M9 14h6"
|
|
1330
|
+
/>
|
|
1331
|
+
</>,
|
|
1332
|
+
);
|
|
1301
1333
|
}
|
|
1302
1334
|
|
|
1303
1335
|
return <></>;
|
|
@@ -29,6 +29,7 @@ export interface MoreMenuItem {
|
|
|
29
29
|
description: string;
|
|
30
30
|
route: Route;
|
|
31
31
|
icon: IconProp;
|
|
32
|
+
iconColor?: string; // Tailwind color class like "bg-blue-500"
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface ComponentProps {
|
|
@@ -277,7 +278,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
277
278
|
{isMoreMenuVisible &&
|
|
278
279
|
(props.moreMenuFooter ? (
|
|
279
280
|
<NavBarMenu footer={props.moreMenuFooter}>
|
|
280
|
-
{props.moreMenuItems.map((item:
|
|
281
|
+
{props.moreMenuItems.map((item: MoreMenuItem) => {
|
|
281
282
|
return (
|
|
282
283
|
<NavBarMenuItem
|
|
283
284
|
key={item.title}
|
|
@@ -285,6 +286,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
285
286
|
description={item.description}
|
|
286
287
|
route={item.route}
|
|
287
288
|
icon={item.icon}
|
|
289
|
+
iconColor={item.iconColor}
|
|
288
290
|
onClick={forceHideMoreMenu}
|
|
289
291
|
/>
|
|
290
292
|
);
|
|
@@ -292,7 +294,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
292
294
|
</NavBarMenu>
|
|
293
295
|
) : (
|
|
294
296
|
<NavBarMenu>
|
|
295
|
-
{props.moreMenuItems.map((item:
|
|
297
|
+
{props.moreMenuItems.map((item: MoreMenuItem) => {
|
|
296
298
|
return (
|
|
297
299
|
<NavBarMenuItem
|
|
298
300
|
key={item.title}
|
|
@@ -300,6 +302,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
300
302
|
description={item.description}
|
|
301
303
|
route={item.route}
|
|
302
304
|
icon={item.icon}
|
|
305
|
+
iconColor={item.iconColor}
|
|
303
306
|
onClick={forceHideMoreMenu}
|
|
304
307
|
/>
|
|
305
308
|
);
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import Link from "../Link/Link";
|
|
2
|
+
import Icon from "../Icon/Icon";
|
|
3
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
2
4
|
import URL from "../../../Types/API/URL";
|
|
3
5
|
import React, { FunctionComponent, ReactElement } from "react";
|
|
4
6
|
|
|
7
|
+
export interface MenuSection {
|
|
8
|
+
title: string;
|
|
9
|
+
children: ReactElement | Array<ReactElement>;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface ComponentProps {
|
|
6
13
|
children: ReactElement | Array<ReactElement>;
|
|
7
14
|
footer?: {
|
|
@@ -11,7 +18,7 @@ export interface ComponentProps {
|
|
|
11
18
|
};
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
const
|
|
21
|
+
const NavBarMenu: FunctionComponent<ComponentProps> = (
|
|
15
22
|
props: ComponentProps,
|
|
16
23
|
): ReactElement => {
|
|
17
24
|
let children: Array<ReactElement>;
|
|
@@ -20,27 +27,54 @@ const NavBarItem: FunctionComponent<ComponentProps> = (
|
|
|
20
27
|
} else {
|
|
21
28
|
children = props.children;
|
|
22
29
|
}
|
|
30
|
+
|
|
31
|
+
// Calculate number of columns based on items count
|
|
32
|
+
const itemCount: number = children.length;
|
|
33
|
+
const columnClass: string =
|
|
34
|
+
itemCount <= 4
|
|
35
|
+
? "lg:grid-cols-2"
|
|
36
|
+
: itemCount <= 6
|
|
37
|
+
? "lg:grid-cols-3"
|
|
38
|
+
: "lg:grid-cols-3";
|
|
39
|
+
const maxWidthClass: string =
|
|
40
|
+
itemCount <= 4
|
|
41
|
+
? "lg:max-w-xl"
|
|
42
|
+
: itemCount <= 6
|
|
43
|
+
? "lg:max-w-2xl"
|
|
44
|
+
: "lg:max-w-3xl";
|
|
45
|
+
|
|
23
46
|
return (
|
|
24
|
-
<div
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
<div
|
|
48
|
+
className={`absolute left-1/2 z-10 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
|
|
49
|
+
>
|
|
50
|
+
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
|
|
51
|
+
{/* Menu Items */}
|
|
52
|
+
<div className={`relative grid gap-1 p-4 ${columnClass}`}>
|
|
27
53
|
{children}
|
|
28
54
|
</div>
|
|
55
|
+
|
|
56
|
+
{/* Footer */}
|
|
29
57
|
{props.footer && (
|
|
30
|
-
<div className="bg-gray-50
|
|
58
|
+
<div className="border-t border-gray-100 bg-gray-50 px-4 py-4">
|
|
31
59
|
<Link
|
|
32
60
|
to={props.footer.link}
|
|
33
61
|
openInNewTab={true}
|
|
34
|
-
className="-
|
|
62
|
+
className="group flex items-center gap-3 rounded-lg p-2.5 -m-2 transition-colors hover:bg-gray-100"
|
|
35
63
|
>
|
|
36
|
-
<
|
|
37
|
-
<
|
|
64
|
+
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-gray-100 ring-1 ring-gray-200 group-hover:bg-gray-200 group-hover:ring-gray-300 transition-all">
|
|
65
|
+
<Icon
|
|
66
|
+
icon={IconProp.GitHub}
|
|
67
|
+
className="h-5 w-5 text-gray-700"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="flex-1 min-w-0 text-left">
|
|
71
|
+
<p className="text-sm font-medium text-gray-900">
|
|
38
72
|
{props.footer.title}
|
|
39
|
-
</
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</
|
|
73
|
+
</p>
|
|
74
|
+
<p className="text-xs text-gray-500">
|
|
75
|
+
{props.footer.description}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
44
78
|
</Link>
|
|
45
79
|
</div>
|
|
46
80
|
)}
|
|
@@ -49,4 +83,4 @@ const NavBarItem: FunctionComponent<ComponentProps> = (
|
|
|
49
83
|
);
|
|
50
84
|
};
|
|
51
85
|
|
|
52
|
-
export default
|
|
86
|
+
export default NavBarMenu;
|
|
@@ -10,24 +10,122 @@ export interface ComponentProps {
|
|
|
10
10
|
icon: IconProp;
|
|
11
11
|
description: string;
|
|
12
12
|
onClick: () => void;
|
|
13
|
+
iconColor?: string; // Tailwind color name like "blue", "purple", "amber"
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const NavBarMenuItem: FunctionComponent<ComponentProps> = (
|
|
16
17
|
props: ComponentProps,
|
|
17
18
|
): ReactElement => {
|
|
19
|
+
// Default to indigo if no color specified
|
|
20
|
+
const colorName: string = props.iconColor || "indigo";
|
|
21
|
+
|
|
22
|
+
// Map color names to their respective Tailwind classes
|
|
23
|
+
const colorClasses: Record<
|
|
24
|
+
string,
|
|
25
|
+
{ bg: string; ring: string; hoverBg: string; hoverRing: string }
|
|
26
|
+
> = {
|
|
27
|
+
purple: {
|
|
28
|
+
bg: "bg-purple-50",
|
|
29
|
+
ring: "ring-purple-200",
|
|
30
|
+
hoverBg: "hover:bg-purple-50",
|
|
31
|
+
hoverRing: "group-hover:ring-purple-300",
|
|
32
|
+
},
|
|
33
|
+
blue: {
|
|
34
|
+
bg: "bg-blue-50",
|
|
35
|
+
ring: "ring-blue-200",
|
|
36
|
+
hoverBg: "hover:bg-blue-50",
|
|
37
|
+
hoverRing: "group-hover:ring-blue-300",
|
|
38
|
+
},
|
|
39
|
+
gray: {
|
|
40
|
+
bg: "bg-gray-100",
|
|
41
|
+
ring: "ring-gray-300",
|
|
42
|
+
hoverBg: "hover:bg-gray-50",
|
|
43
|
+
hoverRing: "group-hover:ring-gray-400",
|
|
44
|
+
},
|
|
45
|
+
amber: {
|
|
46
|
+
bg: "bg-amber-50",
|
|
47
|
+
ring: "ring-amber-200",
|
|
48
|
+
hoverBg: "hover:bg-amber-50",
|
|
49
|
+
hoverRing: "group-hover:ring-amber-300",
|
|
50
|
+
},
|
|
51
|
+
green: {
|
|
52
|
+
bg: "bg-green-50",
|
|
53
|
+
ring: "ring-green-200",
|
|
54
|
+
hoverBg: "hover:bg-green-50",
|
|
55
|
+
hoverRing: "group-hover:ring-green-300",
|
|
56
|
+
},
|
|
57
|
+
cyan: {
|
|
58
|
+
bg: "bg-cyan-50",
|
|
59
|
+
ring: "ring-cyan-200",
|
|
60
|
+
hoverBg: "hover:bg-cyan-50",
|
|
61
|
+
hoverRing: "group-hover:ring-cyan-300",
|
|
62
|
+
},
|
|
63
|
+
slate: {
|
|
64
|
+
bg: "bg-slate-100",
|
|
65
|
+
ring: "ring-slate-300",
|
|
66
|
+
hoverBg: "hover:bg-slate-50",
|
|
67
|
+
hoverRing: "group-hover:ring-slate-400",
|
|
68
|
+
},
|
|
69
|
+
indigo: {
|
|
70
|
+
bg: "bg-indigo-50",
|
|
71
|
+
ring: "ring-indigo-200",
|
|
72
|
+
hoverBg: "hover:bg-indigo-50",
|
|
73
|
+
hoverRing: "group-hover:ring-indigo-300",
|
|
74
|
+
},
|
|
75
|
+
rose: {
|
|
76
|
+
bg: "bg-rose-50",
|
|
77
|
+
ring: "ring-rose-200",
|
|
78
|
+
hoverBg: "hover:bg-rose-50",
|
|
79
|
+
hoverRing: "group-hover:ring-rose-300",
|
|
80
|
+
},
|
|
81
|
+
violet: {
|
|
82
|
+
bg: "bg-violet-50",
|
|
83
|
+
ring: "ring-violet-200",
|
|
84
|
+
hoverBg: "hover:bg-violet-50",
|
|
85
|
+
hoverRing: "group-hover:ring-violet-300",
|
|
86
|
+
},
|
|
87
|
+
orange: {
|
|
88
|
+
bg: "bg-orange-50",
|
|
89
|
+
ring: "ring-orange-200",
|
|
90
|
+
hoverBg: "hover:bg-orange-50",
|
|
91
|
+
hoverRing: "group-hover:ring-orange-300",
|
|
92
|
+
},
|
|
93
|
+
stone: {
|
|
94
|
+
bg: "bg-stone-100",
|
|
95
|
+
ring: "ring-stone-300",
|
|
96
|
+
hoverBg: "hover:bg-stone-50",
|
|
97
|
+
hoverRing: "group-hover:ring-stone-400",
|
|
98
|
+
},
|
|
99
|
+
sky: {
|
|
100
|
+
bg: "bg-sky-50",
|
|
101
|
+
ring: "ring-sky-200",
|
|
102
|
+
hoverBg: "hover:bg-sky-50",
|
|
103
|
+
hoverRing: "group-hover:ring-sky-300",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const colors: {
|
|
108
|
+
bg: string;
|
|
109
|
+
ring: string;
|
|
110
|
+
hoverBg: string;
|
|
111
|
+
hoverRing: string;
|
|
112
|
+
} = colorClasses[colorName] || colorClasses["indigo"]!;
|
|
113
|
+
|
|
18
114
|
return (
|
|
19
115
|
<div className="dropdown">
|
|
20
116
|
<Link
|
|
21
117
|
onClick={props.onClick}
|
|
22
118
|
to={props.route}
|
|
23
|
-
className=
|
|
119
|
+
className={`group flex items-center gap-3 rounded-lg p-2.5 transition-colors ${colors.hoverBg}`}
|
|
24
120
|
>
|
|
25
|
-
<div
|
|
26
|
-
|
|
121
|
+
<div
|
|
122
|
+
className={`flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg ${colors.bg} ring-1 ${colors.ring} ${colors.hoverRing} transition-all`}
|
|
123
|
+
>
|
|
124
|
+
<Icon icon={props.icon} className="h-4 w-4 text-gray-600" />
|
|
27
125
|
</div>
|
|
28
|
-
<div className="
|
|
29
|
-
<p className="text-
|
|
30
|
-
<p className="text-
|
|
126
|
+
<div className="flex-1 min-w-0 text-left">
|
|
127
|
+
<p className="text-sm font-medium text-gray-900">{props.title}</p>
|
|
128
|
+
<p className="text-xs text-gray-500">{props.description}</p>
|
|
31
129
|
</div>
|
|
32
130
|
</Link>
|
|
33
131
|
</div>
|
|
@@ -266,8 +266,17 @@ export default class AIAgentDataAPI {
|
|
|
266
266
|
if (!codeRepository.gitHubAppInstallationId) {
|
|
267
267
|
return Response.sendErrorResponse(req, res, new BadDataException("No GitHub App installation ID found for this repository"));
|
|
268
268
|
}
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
/*
|
|
270
|
+
* Generate GitHub installation access token with write permissions
|
|
271
|
+
* Required for AI Agent to push branches and create pull requests
|
|
272
|
+
*/
|
|
273
|
+
const tokenData = await GitHubUtil.getInstallationAccessToken(codeRepository.gitHubAppInstallationId, {
|
|
274
|
+
permissions: {
|
|
275
|
+
contents: "write", // Required for pushing branches
|
|
276
|
+
pull_requests: "write", // Required for creating PRs
|
|
277
|
+
metadata: "read", // Required for reading repository metadata
|
|
278
|
+
},
|
|
279
|
+
});
|
|
271
280
|
const repositoryUrl = `https://github.com/${codeRepository.organizationName}/${codeRepository.repositoryName}.git`;
|
|
272
281
|
logger.debug(`Generated access token for repository ${codeRepository.organizationName}/${codeRepository.repositoryName}`);
|
|
273
282
|
return Response.sendJsonObjectResponse(req, res, {
|