mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add Code Repository management features
- Implement CodeRepositoryService for database interactions. - Enhance GitHub utility functions for app authentication and repository management. - Introduce new permissions for Code Repository actions (create, delete, edit, read). - Create Code Repository pages and routes in the dashboard. - Add side menu and breadcrumbs for Code Repository navigation. - Implement settings and delete functionality for Code Repositories. - Update Helm chart to include GitHub App configuration options. - Modify example environment configuration to include GitHub App credentials.
This commit is contained in:
@@ -497,6 +497,7 @@ import ScheduledMaintenanceFeedService, {
|
||||
|
||||
import SlackAPI from "Common/Server/API/SlackAPI";
|
||||
import MicrosoftTeamsAPI from "Common/Server/API/MicrosoftTeamsAPI";
|
||||
import GitHubAPI from "Common/Server/API/GitHubAPI";
|
||||
|
||||
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService, {
|
||||
@@ -1628,6 +1629,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new MicrosoftTeamsAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new GitHubAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new GlobalConfigAPI().getRouter(),
|
||||
|
||||
664
Common/Models/DatabaseModels/CodeRepository.ts
Normal file
664
Common/Models/DatabaseModels/CodeRepository.ts
Normal file
@@ -0,0 +1,664 @@
|
||||
import Label from "./Label";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import AccessControlColumn from "../../Types/Database/AccessControlColumn";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import UniqueColumnBy from "../../Types/Database/UniqueColumnBy";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import CodeRepositoryType from "../../Types/CodeRepository/CodeRepositoryType";
|
||||
import URL from "../../Types/API/URL";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@AccessControlColumn("labels")
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Growth,
|
||||
read: PlanType.Growth,
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.DeleteCodeRepository,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/code-repository"))
|
||||
@SlugifyColumn("name", "slug")
|
||||
@TableMetadata({
|
||||
tableName: "CodeRepository",
|
||||
singularName: "Code Repository",
|
||||
pluralName: "Code Repositories",
|
||||
icon: IconProp.Code,
|
||||
tableDescription:
|
||||
"Connect and manage code repositories from GitHub, GitLab, and other providers",
|
||||
})
|
||||
@Entity({
|
||||
name: "CodeRepository",
|
||||
})
|
||||
export default class CodeRepository extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
example: "5f8b9c0d-e1a2-4b3c-8d5e-6f7a8b9c0d1e",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Name",
|
||||
description: "A friendly name for this code repository",
|
||||
example: "My Backend API",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
@UniqueColumnBy("projectId")
|
||||
public name?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
unique: true,
|
||||
type: TableColumnType.Slug,
|
||||
computed: true,
|
||||
title: "Slug",
|
||||
description: "Friendly globally unique name for your object",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Slug,
|
||||
length: ColumnLength.Slug,
|
||||
})
|
||||
public slug?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Description",
|
||||
description: "A description of this code repository",
|
||||
example: "Main backend API service for user authentication and data access",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Repository Hosted At",
|
||||
description: "Where is this repository hosted (GitHub, GitLab, etc.)",
|
||||
example: "GitHub",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public repositoryHostedAt?: CodeRepositoryType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Organization Name",
|
||||
description: "GitHub organization or username that owns this repository",
|
||||
example: "my-organization",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public organizationName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Repository Name",
|
||||
description: "The name of the repository",
|
||||
example: "my-backend-api",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public repositoryName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Main Branch Name",
|
||||
description: "The name of the main/default branch",
|
||||
example: "main",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
default: "main",
|
||||
})
|
||||
public mainBranchName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongURL,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Repository URL",
|
||||
description: "The HTTPS URL to the repository",
|
||||
example: "https://github.com/my-organization/my-backend-api",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongURL,
|
||||
transformer: URL.getDatabaseTransformer(),
|
||||
})
|
||||
public repositoryUrl?: URL = undefined;
|
||||
|
||||
// GitHub App specific fields
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "GitHub App Installation ID",
|
||||
description:
|
||||
"The GitHub App installation ID used to authenticate with this repository",
|
||||
example: "12345678",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public gitHubAppInstallationId?: string = undefined;
|
||||
|
||||
// GitLab specific fields
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "GitLab Project ID",
|
||||
description: "The GitLab project ID for this repository",
|
||||
example: "12345678",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public gitLabProjectId?: string = undefined;
|
||||
|
||||
// Webhook secret for verifying incoming webhooks
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
canReadOnRelationQuery: false,
|
||||
title: "Secret Token",
|
||||
description: "Secret token used to verify incoming webhooks",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public secretToken?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
example: "b2c3d4e5-f6a7-8901-bcde-f12345678901",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateCodeRepository,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadCodeRepository,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditCodeRepository,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: Label,
|
||||
title: "Labels",
|
||||
description:
|
||||
"Relation to Labels Array where this object is categorized in.",
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return Label;
|
||||
},
|
||||
{ eager: false },
|
||||
)
|
||||
@JoinTable({
|
||||
name: "CodeRepositoryLabel",
|
||||
inverseJoinColumn: {
|
||||
name: "labelId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
joinColumn: {
|
||||
name: "codeRepositoryId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public labels?: Array<Label> = undefined;
|
||||
}
|
||||
@@ -96,6 +96,7 @@ import ScheduledMaintenanceStateTimeline from "./ScheduledMaintenanceStateTimeli
|
||||
import ServiceCatalog from "./ServiceCatalog";
|
||||
import ServiceCatalogOwnerTeam from "./ServiceCatalogOwnerTeam";
|
||||
import ServiceCatalogOwnerUser from "./ServiceCatalogOwnerUser";
|
||||
import CodeRepository from "./CodeRepository";
|
||||
// Short link.
|
||||
import ShortLink from "./ShortLink";
|
||||
// SMS
|
||||
@@ -369,6 +370,9 @@ const AllModelTypes: Array<{
|
||||
ServiceCatalogMonitor,
|
||||
ServiceCatalogTelemetryService,
|
||||
|
||||
// Code Repository
|
||||
CodeRepository,
|
||||
|
||||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
|
||||
330
Common/Server/API/GitHubAPI.ts
Normal file
330
Common/Server/API/GitHubAPI.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import logger from "../Utils/Logger";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import {
|
||||
DashboardClientUrl,
|
||||
GitHubAppClientId,
|
||||
} from "../EnvironmentConfig";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import GitHubUtil, {
|
||||
GitHubRepository,
|
||||
} from "../Utils/CodeRepository/GitHub/GitHub";
|
||||
import CodeRepositoryService from "../Services/CodeRepositoryService";
|
||||
import CodeRepository from "../../Models/DatabaseModels/CodeRepository";
|
||||
import CodeRepositoryType from "../../Types/CodeRepository/CodeRepositoryType";
|
||||
import URL from "../../Types/API/URL";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
|
||||
export default class GitHubAPI {
|
||||
public getRouter(): ExpressRouter {
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// GitHub App installation callback
|
||||
// This is called after a user installs the GitHub App
|
||||
router.get(
|
||||
"/github/auth/:projectId/:userId/callback",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const projectId: string | undefined =
|
||||
req.params["projectId"]?.toString();
|
||||
const userId: string | undefined = req.params["userId"]?.toString();
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Project ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("User ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// GitHub sends installation_id in query params after app installation
|
||||
const installationId: string | undefined =
|
||||
req.query["installation_id"]?.toString();
|
||||
|
||||
if (!installationId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Installation ID is required. Please install the GitHub App first.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Store the installation ID - we'll create repositories when user selects them
|
||||
// For now, redirect back to dashboard with installation ID
|
||||
const redirectUrl: string = `${DashboardClientUrl.toString()}/dashboard/${projectId}/code-repository?installation_id=${installationId}`;
|
||||
|
||||
return Response.redirect(req, res, URL.fromString(redirectUrl));
|
||||
} catch (error) {
|
||||
logger.error("GitHub Auth Callback Error:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
error instanceof Error
|
||||
? new BadDataException(error.message)
|
||||
: new BadDataException("An error occurred"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Initiate GitHub App installation
|
||||
router.get(
|
||||
"/github/auth/:projectId/:userId/install",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
if (!GitHubAppClientId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"GitHub App is not configured. Please set GITHUB_APP_CLIENT_ID.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const projectId: string | undefined =
|
||||
req.params["projectId"]?.toString();
|
||||
const userId: string | undefined = req.params["userId"]?.toString();
|
||||
|
||||
if (!projectId || !userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Project ID and User ID are required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to GitHub App installation page
|
||||
// The state parameter helps us track the installation
|
||||
const state: string = Buffer.from(
|
||||
JSON.stringify({ projectId, userId }),
|
||||
).toString("base64");
|
||||
|
||||
const installUrl: string = `https://github.com/apps/${GitHubAppClientId}/installations/new?state=${state}`;
|
||||
|
||||
return Response.redirect(req, res, URL.fromString(installUrl));
|
||||
} catch (error) {
|
||||
logger.error("GitHub Install Redirect Error:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
error instanceof Error
|
||||
? new BadDataException(error.message)
|
||||
: new BadDataException("An error occurred"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// List repositories for an installation
|
||||
router.get(
|
||||
"/github/repositories/:projectId/:installationId",
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const installationId: string | undefined =
|
||||
req.params["installationId"]?.toString();
|
||||
|
||||
if (!installationId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Installation ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const repositories: Array<GitHubRepository> =
|
||||
await GitHubUtil.listRepositoriesForInstallation(installationId);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
repositories: repositories as unknown,
|
||||
} as JSONObject);
|
||||
} catch (error) {
|
||||
logger.error("GitHub List Repositories Error:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
error instanceof Error
|
||||
? new BadDataException(error.message)
|
||||
: new BadDataException("An error occurred"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Connect a repository to a project
|
||||
router.post(
|
||||
"/github/repository/connect",
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const projectId: string | undefined = body["projectId"]?.toString();
|
||||
const installationId: string | undefined =
|
||||
body["installationId"]?.toString();
|
||||
const repositoryName: string | undefined =
|
||||
body["repositoryName"]?.toString();
|
||||
const organizationName: string | undefined =
|
||||
body["organizationName"]?.toString();
|
||||
const name: string | undefined = body["name"]?.toString();
|
||||
const defaultBranch: string | undefined =
|
||||
body["defaultBranch"]?.toString();
|
||||
const repositoryUrl: string | undefined =
|
||||
body["repositoryUrl"]?.toString();
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Project ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!installationId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Installation ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!repositoryName) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Repository name is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!organizationName) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Organization name is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Create the code repository record
|
||||
const codeRepository: CodeRepository = new CodeRepository();
|
||||
codeRepository.projectId = new ObjectID(projectId);
|
||||
codeRepository.name = name || `${organizationName}/${repositoryName}`;
|
||||
codeRepository.repositoryHostedAt = CodeRepositoryType.GitHub;
|
||||
codeRepository.organizationName = organizationName;
|
||||
codeRepository.repositoryName = repositoryName;
|
||||
codeRepository.mainBranchName = defaultBranch || "main";
|
||||
codeRepository.gitHubAppInstallationId = installationId;
|
||||
|
||||
if (repositoryUrl) {
|
||||
codeRepository.repositoryUrl = URL.fromString(repositoryUrl);
|
||||
}
|
||||
|
||||
const createdRepository: CodeRepository =
|
||||
await CodeRepositoryService.create({
|
||||
data: codeRepository,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
repository: BaseModel.toJSON(createdRepository, CodeRepository),
|
||||
} as JSONObject);
|
||||
} catch (error) {
|
||||
logger.error("GitHub Connect Repository Error:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
error instanceof Error
|
||||
? new BadDataException(error.message)
|
||||
: new BadDataException("An error occurred"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// GitHub webhook handler
|
||||
router.post(
|
||||
"/github/webhook",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const signature: string | undefined = req.headers[
|
||||
"x-hub-signature-256"
|
||||
] as string | undefined;
|
||||
const event: string | undefined = req.headers["x-github-event"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!signature) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Missing webhook signature"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get raw body for signature verification
|
||||
const rawBody: string = JSON.stringify(req.body);
|
||||
|
||||
// Verify webhook signature
|
||||
const isValid: boolean = GitHubUtil.verifyWebhookSignature(
|
||||
rawBody,
|
||||
signature,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid webhook signature"),
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Received GitHub webhook event: ${event}`);
|
||||
|
||||
// Handle different webhook events here
|
||||
// For now, just acknowledge receipt
|
||||
// Future: Handle push, pull_request, check_run events
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
} as JSONObject);
|
||||
} catch (error) {
|
||||
logger.error("GitHub Webhook Error:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
error instanceof Error
|
||||
? new BadDataException(error.message)
|
||||
: new BadDataException("An error occurred"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
|
||||
"DISABLE_TELEMETRY",
|
||||
"SLACK_APP_CLIENT_ID",
|
||||
"MICROSOFT_TEAMS_APP_CLIENT_ID",
|
||||
"GITHUB_APP_CLIENT_ID",
|
||||
"CAPTCHA_ENABLED",
|
||||
"CAPTCHA_SITE_KEY",
|
||||
];
|
||||
@@ -458,6 +459,18 @@ export const MicrosoftTeamsAppClientSecret: string | null =
|
||||
export const MicrosoftTeamsAppTenantId: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_TENANT_ID"] || null;
|
||||
|
||||
// GitHub App Configuration
|
||||
export const GitHubAppId: string | null =
|
||||
process.env["GITHUB_APP_ID"] || null;
|
||||
export const GitHubAppClientId: string | null =
|
||||
process.env["GITHUB_APP_CLIENT_ID"] || null;
|
||||
export const GitHubAppClientSecret: string | null =
|
||||
process.env["GITHUB_APP_CLIENT_SECRET"] || null;
|
||||
export const GitHubAppPrivateKey: string | null =
|
||||
process.env["GITHUB_APP_PRIVATE_KEY"] || null;
|
||||
export const GitHubAppWebhookSecret: string | null =
|
||||
process.env["GITHUB_APP_WEBHOOK_SECRET"] || null;
|
||||
|
||||
// VAPID Configuration for Web Push Notifications
|
||||
export const VapidPublicKey: string | undefined =
|
||||
process.env["VAPID_PUBLIC_KEY"] || undefined;
|
||||
|
||||
10
Common/Server/Services/CodeRepositoryService.ts
Normal file
10
Common/Server/Services/CodeRepositoryService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/CodeRepository";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -10,6 +10,29 @@ import OneUptimeDate from "../../../../Types/Date";
|
||||
import { JSONArray, JSONObject } from "../../../../Types/JSON";
|
||||
import API from "../../../../Utils/API";
|
||||
import CaptureSpan from "../../Telemetry/CaptureSpan";
|
||||
import {
|
||||
GitHubAppId,
|
||||
GitHubAppPrivateKey,
|
||||
GitHubAppWebhookSecret,
|
||||
} from "../../../EnvironmentConfig";
|
||||
import BadDataException from "../../../../Types/Exception/BadDataException";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
export interface GitHubRepository {
|
||||
id: number;
|
||||
name: string;
|
||||
fullName: string;
|
||||
private: boolean;
|
||||
htmlUrl: string;
|
||||
description: string | null;
|
||||
defaultBranch: string;
|
||||
ownerLogin: string;
|
||||
}
|
||||
|
||||
export interface GitHubInstallationToken {
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export default class GitHubUtil extends HostedCodeRepository {
|
||||
private getPullRequestFromJSONObject(data: {
|
||||
@@ -258,4 +281,198 @@ export default class GitHubUtil extends HostedCodeRepository {
|
||||
repositoryName: data.repositoryName,
|
||||
});
|
||||
}
|
||||
|
||||
// GitHub App Authentication Methods
|
||||
|
||||
/**
|
||||
* Generates a JWT for GitHub App authentication
|
||||
* @returns JWT string valid for 10 minutes
|
||||
*/
|
||||
@CaptureSpan()
|
||||
public static generateAppJWT(): string {
|
||||
if (!GitHubAppId) {
|
||||
throw new BadDataException("GITHUB_APP_ID environment variable is not set");
|
||||
}
|
||||
|
||||
if (!GitHubAppPrivateKey) {
|
||||
throw new BadDataException(
|
||||
"GITHUB_APP_PRIVATE_KEY environment variable is not set",
|
||||
);
|
||||
}
|
||||
|
||||
const now: number = Math.floor(Date.now() / 1000);
|
||||
const payload: JSONObject = {
|
||||
iat: now - 60, // Issued at time (60 seconds in the past to allow for clock drift)
|
||||
exp: now + 600, // Expiration time (10 minutes from now)
|
||||
iss: GitHubAppId,
|
||||
};
|
||||
|
||||
// Create JWT header
|
||||
const header: JSONObject = {
|
||||
alg: "RS256",
|
||||
typ: "JWT",
|
||||
};
|
||||
|
||||
const encodedHeader: string = Buffer.from(JSON.stringify(header)).toString(
|
||||
"base64url",
|
||||
);
|
||||
const encodedPayload: string = Buffer.from(JSON.stringify(payload)).toString(
|
||||
"base64url",
|
||||
);
|
||||
|
||||
const signatureInput: string = `${encodedHeader}.${encodedPayload}`;
|
||||
|
||||
// Sign with private key
|
||||
const sign: crypto.Sign = crypto.createSign("RSA-SHA256");
|
||||
sign.update(signatureInput);
|
||||
const signature: string = sign.sign(GitHubAppPrivateKey, "base64url");
|
||||
|
||||
return `${signatureInput}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an installation access token for a GitHub App installation
|
||||
* @param installationId - The GitHub App installation ID
|
||||
* @returns Installation token and expiration date
|
||||
*/
|
||||
@CaptureSpan()
|
||||
public static async getInstallationAccessToken(
|
||||
installationId: string,
|
||||
): Promise<GitHubInstallationToken> {
|
||||
const jwt: string = GitHubUtil.generateAppJWT();
|
||||
|
||||
const url: URL = URL.fromString(
|
||||
`https://api.github.com/app/installations/${installationId}/access_tokens`,
|
||||
);
|
||||
|
||||
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post({
|
||||
url: url,
|
||||
data: {},
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (result instanceof HTTPErrorResponse) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
return {
|
||||
token: result.data["token"] as string,
|
||||
expiresAt: OneUptimeDate.fromString(result.data["expires_at"] as string),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists repositories accessible to a GitHub App installation
|
||||
* @param installationId - The GitHub App installation ID
|
||||
* @returns Array of repositories
|
||||
*/
|
||||
@CaptureSpan()
|
||||
public static async listRepositoriesForInstallation(
|
||||
installationId: string,
|
||||
): Promise<Array<GitHubRepository>> {
|
||||
const tokenData: GitHubInstallationToken =
|
||||
await GitHubUtil.getInstallationAccessToken(installationId);
|
||||
|
||||
const allRepositories: Array<GitHubRepository> = [];
|
||||
let page: number = 1;
|
||||
let hasMore: boolean = true;
|
||||
|
||||
while (hasMore) {
|
||||
const url: URL = URL.fromString(
|
||||
`https://api.github.com/installation/repositories?per_page=100&page=${page}`,
|
||||
);
|
||||
|
||||
const result: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.get(
|
||||
{
|
||||
url: url,
|
||||
data: {},
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.token}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (result instanceof HTTPErrorResponse) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
const repositories: JSONArray =
|
||||
(result.data["repositories"] as JSONArray) || [];
|
||||
|
||||
for (const repo of repositories) {
|
||||
const repoData: JSONObject = repo as JSONObject;
|
||||
const owner: JSONObject = repoData["owner"] as JSONObject;
|
||||
|
||||
allRepositories.push({
|
||||
id: repoData["id"] as number,
|
||||
name: repoData["name"] as string,
|
||||
fullName: repoData["full_name"] as string,
|
||||
private: repoData["private"] as boolean,
|
||||
htmlUrl: repoData["html_url"] as string,
|
||||
description: (repoData["description"] as string) || null,
|
||||
defaultBranch: repoData["default_branch"] as string,
|
||||
ownerLogin: owner["login"] as string,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
if (repositories.length < 100) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
return allRepositories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a GitHub webhook signature
|
||||
* @param payload - The raw request body
|
||||
* @param signature - The X-Hub-Signature-256 header value
|
||||
* @returns true if signature is valid
|
||||
*/
|
||||
public static verifyWebhookSignature(
|
||||
payload: string,
|
||||
signature: string,
|
||||
): boolean {
|
||||
if (!GitHubAppWebhookSecret) {
|
||||
logger.warn("GITHUB_APP_WEBHOOK_SECRET is not set, skipping verification");
|
||||
return true;
|
||||
}
|
||||
|
||||
const expectedSignature: string = `sha256=${crypto
|
||||
.createHmac("sha256", GitHubAppWebhookSecret)
|
||||
.update(payload)
|
||||
.digest("hex")}`;
|
||||
|
||||
try {
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature) as Uint8Array,
|
||||
Buffer.from(expectedSignature) as Uint8Array,
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the GitHub App installation URL for a project to install the app
|
||||
* @returns The installation URL
|
||||
*/
|
||||
public static getAppInstallationUrl(): string {
|
||||
if (!GitHubAppId) {
|
||||
throw new BadDataException("GITHUB_APP_ID environment variable is not set");
|
||||
}
|
||||
|
||||
// This is the standard GitHub App installation URL format
|
||||
// The app slug would typically come from another env var, but we can use the client ID approach
|
||||
return `https://github.com/apps`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
enum CodeRepositoryType {
|
||||
GitHub = "GitHub",
|
||||
// GitLab = 'GitLab',
|
||||
GitLab = "GitLab",
|
||||
}
|
||||
|
||||
export default CodeRepositoryType;
|
||||
|
||||
@@ -624,6 +624,12 @@ enum Permission {
|
||||
EditServiceCatalogTelemetryService = "EditServiceCatalogTelemetryService",
|
||||
ReadServiceCatalogTelemetryService = "ReadServiceCatalogTelemetryService",
|
||||
|
||||
// Code Repository
|
||||
CreateCodeRepository = "CreateCodeRepository",
|
||||
DeleteCodeRepository = "DeleteCodeRepository",
|
||||
EditCodeRepository = "EditCodeRepository",
|
||||
ReadCodeRepository = "ReadCodeRepository",
|
||||
|
||||
CreateProbeOwnerTeam = "CreateProbeOwnerTeam",
|
||||
DeleteProbeOwnerTeam = "DeleteProbeOwnerTeam",
|
||||
EditProbeOwnerTeam = "EditProbeOwnerTeam",
|
||||
@@ -3417,6 +3423,40 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
// Code Repository Permissions
|
||||
{
|
||||
permission: Permission.CreateCodeRepository,
|
||||
title: "Create Code Repository",
|
||||
description:
|
||||
"This permission can create Code Repositories in this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: true,
|
||||
},
|
||||
{
|
||||
permission: Permission.DeleteCodeRepository,
|
||||
title: "Delete Code Repository",
|
||||
description:
|
||||
"This permission can delete Code Repositories of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: true,
|
||||
},
|
||||
{
|
||||
permission: Permission.EditCodeRepository,
|
||||
title: "Edit Code Repository",
|
||||
description:
|
||||
"This permission can edit Code Repositories of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: true,
|
||||
},
|
||||
{
|
||||
permission: Permission.ReadCodeRepository,
|
||||
title: "Read Code Repository",
|
||||
description:
|
||||
"This permission can read Code Repositories of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: true,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateTelemetryServiceTraces,
|
||||
title: "Create Telemetry Service Traces",
|
||||
|
||||
@@ -93,6 +93,11 @@ const ServiceCatalogRoutes: React.LazyExoticComponent<
|
||||
> = lazy(() => {
|
||||
return import("./Routes/ServiceCatalogRoutes");
|
||||
});
|
||||
const CodeRepositoryRoutes: React.LazyExoticComponent<
|
||||
React.FunctionComponent<PageComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("./Routes/CodeRepositoryRoutes");
|
||||
});
|
||||
const IncidentsRoutes: React.LazyExoticComponent<
|
||||
React.FunctionComponent<PageComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -448,6 +453,12 @@ const App: () => JSX.Element = () => {
|
||||
element={<ServiceCatalogRoutes {...commonPageProps} />}
|
||||
/>
|
||||
|
||||
{/* Code Repository */}
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.CODE_REPOSITORY_ROOT]?.toString() || ""}
|
||||
element={<CodeRepositoryRoutes {...commonPageProps} />}
|
||||
/>
|
||||
|
||||
{/* Incidents */}
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.INCIDENTS_ROOT]?.toString() || ""}
|
||||
|
||||
@@ -91,6 +91,14 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
),
|
||||
icon: IconProp.SquareStack,
|
||||
},
|
||||
{
|
||||
title: "Code Repositories",
|
||||
description: "Connect and manage your GitHub and GitLab repositories.",
|
||||
route: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.CODE_REPOSITORY] as Route,
|
||||
),
|
||||
icon: IconProp.Code,
|
||||
},
|
||||
{
|
||||
title: "Scheduled Maintenance",
|
||||
description: "Manage your scheduled maintenance events.",
|
||||
|
||||
205
Dashboard/src/Pages/CodeRepository/CodeRepository.tsx
Normal file
205
Dashboard/src/Pages/CodeRepository/CodeRepository.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import DropdownUtil from "Common/UI/Utils/Dropdown";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const CodeRepositoryPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<ModelTable<CodeRepository>
|
||||
modelType={CodeRepository}
|
||||
id="code-repository-table"
|
||||
userPreferencesKey="code-repository-table"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
name="Code Repositories"
|
||||
isViewable={true}
|
||||
cardProps={{
|
||||
title: "Code Repositories",
|
||||
description:
|
||||
"Connect and manage your GitHub and GitLab repositories here.",
|
||||
}}
|
||||
showViewIdButton={true}
|
||||
noItemsMessage={"No repositories connected."}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Repository Name",
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Description",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryHostedAt: true,
|
||||
},
|
||||
title: "Repository Host",
|
||||
description: "Where is this repository hosted?",
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
required: true,
|
||||
placeholder: "Select Host",
|
||||
dropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(CodeRepositoryType),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
organizationName: true,
|
||||
},
|
||||
title: "Organization / Username",
|
||||
description: "The GitHub organization or username that owns the repository.",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Organization Name",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryName: true,
|
||||
},
|
||||
title: "Repository Name",
|
||||
description: "The name of the repository.",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Repository Name",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
title: "Main Branch",
|
||||
description: "The main branch of the repository (e.g., main, master).",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "main",
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryHostedAt: true,
|
||||
},
|
||||
title: "Repository Host",
|
||||
type: FieldType.Dropdown,
|
||||
filterDropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(CodeRepositoryType),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
organizationName: true,
|
||||
},
|
||||
title: "Organization",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryName: true,
|
||||
},
|
||||
title: "Repository",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: "Labels",
|
||||
type: FieldType.EntityArray,
|
||||
filterEntityType: Label,
|
||||
filterQuery: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: "name",
|
||||
value: "_id",
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryHostedAt: true,
|
||||
},
|
||||
title: "Host",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
organizationName: true,
|
||||
},
|
||||
title: "Organization",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryName: true,
|
||||
},
|
||||
title: "Repository",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
title: "Main Branch",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: "Labels",
|
||||
type: FieldType.EntityArray,
|
||||
getElement: (item: CodeRepository): ReactElement => {
|
||||
return <LabelsElement labels={item["labels"] || []} />;
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryPage;
|
||||
25
Dashboard/src/Pages/CodeRepository/Layout.tsx
Normal file
25
Dashboard/src/Pages/CodeRepository/Layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getCodeRepositoryBreadcrumbs } from "../../Utils/Breadcrumbs";
|
||||
import { RouteUtil } from "../../Utils/RouteMap";
|
||||
import LayoutPageComponentProps from "../LayoutPageComponentProps";
|
||||
import SideMenu from "./SideMenu";
|
||||
import Page from "Common/UI/Components/Page/Page";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
const CodeRepositoryLayout: FunctionComponent<LayoutPageComponentProps> = (
|
||||
_props: LayoutPageComponentProps,
|
||||
): ReactElement => {
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<Page
|
||||
title={"Code Repositories"}
|
||||
sideMenu={<SideMenu />}
|
||||
breadcrumbLinks={getCodeRepositoryBreadcrumbs(path)}
|
||||
>
|
||||
<Outlet />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryLayout;
|
||||
31
Dashboard/src/Pages/CodeRepository/SideMenu.tsx
Normal file
31
Dashboard/src/Pages/CodeRepository/SideMenu.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import SideMenu, {
|
||||
SideMenuSectionProps,
|
||||
} from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const CodeRepositorySideMenu: FunctionComponent = (): ReactElement => {
|
||||
const sections: SideMenuSectionProps[] = [
|
||||
{
|
||||
title: "Code Repositories",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "All Repositories",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.CODE_REPOSITORY] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.List,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
};
|
||||
|
||||
export default CodeRepositorySideMenu;
|
||||
29
Dashboard/src/Pages/CodeRepository/View/Delete.tsx
Normal file
29
Dashboard/src/Pages/CodeRepository/View/Delete.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap from "../../../Utils/RouteMap";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ModelDelete from "Common/UI/Components/ModelDelete/ModelDelete";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const CodeRepositoryDelete: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelDelete
|
||||
modelType={CodeRepository}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(RouteMap[PageMap.CODE_REPOSITORY] as Route);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryDelete;
|
||||
196
Dashboard/src/Pages/CodeRepository/View/Index.tsx
Normal file
196
Dashboard/src/Pages/CodeRepository/View/Index.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import DropdownUtil from "Common/UI/Utils/Dropdown";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const CodeRepositoryView: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CardModelDetail<CodeRepository>
|
||||
name="Repository > Repository Details"
|
||||
cardProps={{
|
||||
title: "Repository Details",
|
||||
description: "Here are more details for this repository.",
|
||||
}}
|
||||
formSteps={[
|
||||
{
|
||||
title: "Repository Info",
|
||||
id: "repository-info",
|
||||
},
|
||||
{
|
||||
title: "Labels",
|
||||
id: "labels",
|
||||
},
|
||||
]}
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
stepId: "repository-info",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Repository Name",
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
stepId: "repository-info",
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Description",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryHostedAt: true,
|
||||
},
|
||||
stepId: "repository-info",
|
||||
title: "Repository Host",
|
||||
description: "Where is this repository hosted?",
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
required: true,
|
||||
placeholder: "Select Host",
|
||||
dropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(CodeRepositoryType),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
organizationName: true,
|
||||
},
|
||||
stepId: "repository-info",
|
||||
title: "Organization / Username",
|
||||
description:
|
||||
"The GitHub organization or username that owns the repository.",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Organization Name",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryName: true,
|
||||
},
|
||||
stepId: "repository-info",
|
||||
title: "Repository Name",
|
||||
description: "The name of the repository.",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "Repository Name",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
stepId: "repository-info",
|
||||
title: "Main Branch",
|
||||
description:
|
||||
"The main branch of the repository (e.g., main, master).",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "main",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: true,
|
||||
},
|
||||
title: "Labels",
|
||||
stepId: "labels",
|
||||
description:
|
||||
"Team members with access to these labels will only be able to access this resource. This is optional and an advanced feature.",
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Label,
|
||||
labelField: "name",
|
||||
valueField: "_id",
|
||||
},
|
||||
required: false,
|
||||
placeholder: "Labels",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: CodeRepository,
|
||||
id: "model-detail-code-repository",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
title: "Repository ID",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryHostedAt: true,
|
||||
},
|
||||
title: "Repository Host",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
organizationName: true,
|
||||
},
|
||||
title: "Organization",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
repositoryName: true,
|
||||
},
|
||||
title: "Repository",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
title: "Main Branch",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: "Labels",
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: CodeRepository): ReactElement => {
|
||||
return <LabelsElement labels={item["labels"] || []} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryView;
|
||||
32
Dashboard/src/Pages/CodeRepository/View/Layout.tsx
Normal file
32
Dashboard/src/Pages/CodeRepository/View/Layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getCodeRepositoryBreadcrumbs } from "../../../Utils/Breadcrumbs";
|
||||
import { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import SideMenu from "./SideMenu";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ModelPage from "Common/UI/Components/Page/ModelPage";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
|
||||
const CodeRepositoryViewLayout: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const { id } = useParams();
|
||||
const modelId: ObjectID = new ObjectID(id || "");
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<ModelPage
|
||||
title="Repository"
|
||||
modelType={CodeRepository}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={getCodeRepositoryBreadcrumbs(path)}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<Outlet />
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryViewLayout;
|
||||
58
Dashboard/src/Pages/CodeRepository/View/Settings.tsx
Normal file
58
Dashboard/src/Pages/CodeRepository/View/Settings.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const CodeRepositorySettings: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CardModelDetail
|
||||
name="Repository Settings"
|
||||
cardProps={{
|
||||
title: "Repository Settings",
|
||||
description: "Configure settings for your repository.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Settings"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
title: "Main Branch",
|
||||
description:
|
||||
"The main branch of the repository (e.g., main, master).",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "main",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: CodeRepository,
|
||||
id: "model-detail-code-repository-settings",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
mainBranchName: true,
|
||||
},
|
||||
title: "Main Branch",
|
||||
description: "The main branch of the repository.",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositorySettings;
|
||||
60
Dashboard/src/Pages/CodeRepository/View/SideMenu.tsx
Normal file
60
Dashboard/src/Pages/CodeRepository/View/SideMenu.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import SideMenu from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem";
|
||||
import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
}
|
||||
|
||||
const CodeRepositorySideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.CODE_REPOSITORY_VIEW] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.CODE_REPOSITORY_VIEW_SETTINGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Delete Repository",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.CODE_REPOSITORY_VIEW_DELETE] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Trash}
|
||||
className="danger-on-hover"
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositorySideMenu;
|
||||
115
Dashboard/src/Routes/CodeRepositoryRoutes.tsx
Normal file
115
Dashboard/src/Routes/CodeRepositoryRoutes.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import Loader from "../Components/Loader/Loader";
|
||||
import ComponentProps from "../Pages/PageComponentProps";
|
||||
import CodeRepositoryViewLayout from "../Pages/CodeRepository/View/Layout";
|
||||
import CodeRepositoryLayout from "../Pages/CodeRepository/Layout";
|
||||
import PageMap from "../Utils/PageMap";
|
||||
import RouteMap, {
|
||||
RouteUtil,
|
||||
CodeRepositoryRoutePath,
|
||||
} from "../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
LazyExoticComponent,
|
||||
ReactElement,
|
||||
Suspense,
|
||||
lazy,
|
||||
} from "react";
|
||||
import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
const CodeRepository: LazyExoticComponent<FunctionComponent<ComponentProps>> =
|
||||
lazy(() => {
|
||||
return import("../Pages/CodeRepository/CodeRepository");
|
||||
});
|
||||
|
||||
const CodeRepositoryView: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/CodeRepository/View/Index");
|
||||
});
|
||||
|
||||
const CodeRepositoryViewDelete: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/CodeRepository/View/Delete");
|
||||
});
|
||||
|
||||
const CodeRepositoryViewSettings: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/CodeRepository/View/Settings");
|
||||
});
|
||||
|
||||
const CodeRepositoryRoutes: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Routes>
|
||||
<PageRoute path="/" element={<CodeRepositoryLayout {...props} />}>
|
||||
<PageRoute
|
||||
path={CodeRepositoryRoutePath[PageMap.CODE_REPOSITORY] || ""}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<CodeRepository
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.CODE_REPOSITORY] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute
|
||||
path={CodeRepositoryRoutePath[PageMap.CODE_REPOSITORY_VIEW] || ""}
|
||||
element={<CodeRepositoryViewLayout {...props} />}
|
||||
>
|
||||
<PageRoute
|
||||
index
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<CodeRepositoryView
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.CODE_REPOSITORY_VIEW] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.CODE_REPOSITORY_VIEW_DELETE,
|
||||
)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<CodeRepositoryViewDelete
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.CODE_REPOSITORY_VIEW_DELETE] as Route
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.CODE_REPOSITORY_VIEW_SETTINGS,
|
||||
)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<CodeRepositoryViewSettings
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.CODE_REPOSITORY_VIEW_SETTINGS] as Route
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRepositoryRoutes;
|
||||
33
Dashboard/src/Utils/Breadcrumbs/CodeRepositoryBreadcrumbs.ts
Normal file
33
Dashboard/src/Utils/Breadcrumbs/CodeRepositoryBreadcrumbs.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import PageMap from "../PageMap";
|
||||
import { BuildBreadcrumbLinksByTitles } from "./Helper";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import Link from "Common/Types/Link";
|
||||
|
||||
export function getCodeRepositoryBreadcrumbs(
|
||||
path: string,
|
||||
): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.CODE_REPOSITORY, [
|
||||
"Project",
|
||||
"Code Repositories",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.CODE_REPOSITORY_VIEW, [
|
||||
"Project",
|
||||
"Code Repositories",
|
||||
"View Repository",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.CODE_REPOSITORY_VIEW_DELETE, [
|
||||
"Project",
|
||||
"Code Repositories",
|
||||
"View Repository",
|
||||
"Delete Repository",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.CODE_REPOSITORY_VIEW_SETTINGS, [
|
||||
"Project",
|
||||
"Code Repositories",
|
||||
"View Repository",
|
||||
"Settings",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
@@ -8,4 +8,5 @@ export * from "./TelemetryBreadcrumbs";
|
||||
export * from "./SettingsBreadcrumbs";
|
||||
export * from "./MonitorGroupBreadcrumbs";
|
||||
export * from "./ServiceCatalogBreadcrumbs";
|
||||
export * from "./CodeRepositoryBreadcrumbs";
|
||||
export * from "./DashboardBreadCrumbs";
|
||||
|
||||
@@ -174,6 +174,13 @@ enum PageMap {
|
||||
SERVICE_CATALOG_VIEW_DEPENDENCIES = "SERVICE_CATALOG_VIEW_DEPENDENCIES",
|
||||
SERVICE_CATALOG_DEPENDENCY_GRAPH = "SERVICE_CATALOG_DEPENDENCY_GRAPH",
|
||||
|
||||
// Code Repository
|
||||
CODE_REPOSITORY_ROOT = "CODE_REPOSITORY_ROOT",
|
||||
CODE_REPOSITORY = "CODE_REPOSITORY",
|
||||
CODE_REPOSITORY_VIEW = "CODE_REPOSITORY_VIEW",
|
||||
CODE_REPOSITORY_VIEW_DELETE = "CODE_REPOSITORY_VIEW_DELETE",
|
||||
CODE_REPOSITORY_VIEW_SETTINGS = "CODE_REPOSITORY_VIEW_SETTINGS",
|
||||
|
||||
DASHBOARDS_ROOT = "DASHBOARDS_ROOT",
|
||||
DASHBOARDS = "DASHBOARDS",
|
||||
DASHBOARD_VIEW = "DASHBOARD_VIEW",
|
||||
|
||||
@@ -46,6 +46,12 @@ export const ServiceCatalogRoutePath: Dictionary<string> = {
|
||||
[PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES]: `${RouteParams.ModelID}/telemetry-service`,
|
||||
};
|
||||
|
||||
export const CodeRepositoryRoutePath: Dictionary<string> = {
|
||||
[PageMap.CODE_REPOSITORY_VIEW]: `${RouteParams.ModelID}`,
|
||||
[PageMap.CODE_REPOSITORY_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
|
||||
[PageMap.CODE_REPOSITORY_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
|
||||
};
|
||||
|
||||
export const WorkflowRoutePath: Dictionary<string> = {
|
||||
[PageMap.WORKFLOWS_LOGS]: "logs",
|
||||
[PageMap.WORKFLOWS_VARIABLES]: "variables",
|
||||
@@ -908,6 +914,34 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
// Code Repository
|
||||
|
||||
[PageMap.CODE_REPOSITORY_ROOT]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/code-repository/*`,
|
||||
),
|
||||
|
||||
[PageMap.CODE_REPOSITORY]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/code-repository`,
|
||||
),
|
||||
|
||||
[PageMap.CODE_REPOSITORY_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/code-repository/${
|
||||
CodeRepositoryRoutePath[PageMap.CODE_REPOSITORY_VIEW]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.CODE_REPOSITORY_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/code-repository/${
|
||||
CodeRepositoryRoutePath[PageMap.CODE_REPOSITORY_VIEW_DELETE]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.CODE_REPOSITORY_VIEW_SETTINGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/code-repository/${
|
||||
CodeRepositoryRoutePath[PageMap.CODE_REPOSITORY_VIEW_SETTINGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
// Dashboards
|
||||
|
||||
[PageMap.DASHBOARDS_ROOT]: new Route(
|
||||
|
||||
@@ -53,6 +53,10 @@ Usage:
|
||||
{{- end }}
|
||||
- name: SLACK_APP_CLIENT_ID
|
||||
value: {{ $.Values.slackApp.clientId | quote }}
|
||||
- name: GITHUB_APP_ID
|
||||
value: {{ $.Values.gitHubApp.id | quote }}
|
||||
- name: GITHUB_APP_CLIENT_ID
|
||||
value: {{ $.Values.gitHubApp.clientId | quote }}
|
||||
- name: HOST
|
||||
value: {{ $.Values.host }}
|
||||
- name: PROVISION_SSL
|
||||
@@ -196,6 +200,15 @@ Usage:
|
||||
- name: MICROSOFT_TEAMS_APP_CLIENT_SECRET
|
||||
value: {{ $.Values.microsoftTeamsApp.clientSecret }}
|
||||
|
||||
- name: GITHUB_APP_CLIENT_SECRET
|
||||
value: {{ $.Values.gitHubApp.clientSecret }}
|
||||
|
||||
- name: GITHUB_APP_PRIVATE_KEY
|
||||
value: {{ $.Values.gitHubApp.privateKey | quote }}
|
||||
|
||||
- name: GITHUB_APP_WEBHOOK_SECRET
|
||||
value: {{ $.Values.gitHubApp.webhookSecret }}
|
||||
|
||||
- name: CAPTCHA_SECRET_KEY
|
||||
value: {{ default "" $.Values.captcha.secretKey | quote }}
|
||||
|
||||
|
||||
@@ -2011,6 +2011,27 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"gitHubApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"clientId": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"privateKey": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"webhookSecret": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"keda": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -797,5 +797,23 @@ microsoftTeamsApp:
|
||||
clientSecret:
|
||||
tenantId:
|
||||
|
||||
# GitHub Example Configuration
|
||||
# gitHubApp:
|
||||
# id: "123456"
|
||||
# clientId: "Iv1.abc123"
|
||||
# clientSecret: "your-client-secret"
|
||||
# privateKey: |
|
||||
# -----BEGIN RSA PRIVATE KEY-----
|
||||
# ...
|
||||
# -----END RSA PRIVATE KEY-----
|
||||
# webhookSecret: "your-webhook-secret"
|
||||
|
||||
gitHubApp:
|
||||
id:
|
||||
clientId:
|
||||
clientSecret:
|
||||
privateKey:
|
||||
webhookSecret:
|
||||
|
||||
keda:
|
||||
enabled: true
|
||||
|
||||
@@ -345,4 +345,13 @@ NGINX_LISTEN_OPTIONS=
|
||||
# The secret value is typically longer and includes more characters
|
||||
MICROSOFT_TEAMS_APP_CLIENT_ID=
|
||||
MICROSOFT_TEAMS_APP_CLIENT_SECRET=
|
||||
MICROSOFT_TEAMS_APP_TENANT_ID=
|
||||
MICROSOFT_TEAMS_APP_TENANT_ID=
|
||||
|
||||
# GitHub App Configuration
|
||||
# Create a GitHub App at https://github.com/settings/apps
|
||||
# Required for connecting GitHub repositories to OneUptime
|
||||
GITHUB_APP_ID=
|
||||
GITHUB_APP_CLIENT_ID=
|
||||
GITHUB_APP_CLIENT_SECRET=
|
||||
GITHUB_APP_PRIVATE_KEY=
|
||||
GITHUB_APP_WEBHOOK_SECRET=
|
||||
Reference in New Issue
Block a user