diff --git a/README.md b/README.md index beed4a4..71cc3d5 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,13 @@ Here is the list of environment variables: #### For the api -- **GITHUB_ID** (required): the Github ID for the created Github OAuth App -- **GITHUB_SECRET** (required): the Github Secret for the created Github OAuth App +- **GIT_PROVIDER_NAME**: The name of the git provider to connect to (github or gitlab) +- **GIT_PROVIDER_TOKEN**: The personal access token of the git provider to connect to - **REDIS_HOST** (required): the host for the Redis server - **REDIS_PORT** (required): the port for the Redis server - **REDIS_PASSWORD** (optional): the password for the Redis server -- **JWT_SECRET** (required): the secret used to decrypt JWT tokens #### For the client -- **GITHUB_ID** (required): the Github ID for the created Github OAuth App -- **GITHUB_SECRET** (required): the Github Secret for the created Github OAuth App - **NEXT_PUBLIC_API_URL** (required): the public URL for the API - **SERVER_API_URL** (required): the private URL for the API for server to server calls -- **NEXTAUTH_SECRET** (required): the secret used to encrypt JWT tokens (must be the same as the `JWT_SECRET` env variable for the api) diff --git a/api/.env b/api/.env index f01c37c..a1d7390 100644 --- a/api/.env +++ b/api/.env @@ -1,8 +1,5 @@ -JWT_SECRET=SECRET -GITHUB_ID=GITHUB_ID -GITHUB_SECRET=GITHUB_SECRET -GITLAB_ID=GITLAB_ID -GITLAB_SECRET=GITLAB_SECRET +GIT_PROVIDER_NAME=github +GIT_PROVIDER_TOKEN=GIT_PROVIDER_TOKEN REDIS_PORT=6379 REDIS_HOST=127.0.0.1 REDIS_PASSWORD=REDIS_PASSWORD \ No newline at end of file diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 2874179..1ad9e84 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -2,7 +2,6 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BullModule } from '@nestjs/bull'; -import { AuthMiddleware } from './auth.middleware'; import { User } from './entities/user.entity'; import { UsersModule } from './modules/users/users.module'; import { Repo } from './entities/repo.entity'; @@ -12,6 +11,9 @@ import { Issue } from './entities/issue.entity'; import { PullRequest } from './entities/pullrequest.entity'; import { GitProviderModule } from './modules/git-provider/gitprovider.module'; import { RepoModule } from './modules/repo/repo.module'; +import { NotificationsModule } from './modules/notifications/notifications.module'; +import { InitService } from './init.service'; +import { AuthMiddleware } from './auth.middleware'; @Module({ imports: [ @@ -36,9 +38,16 @@ import { RepoModule } from './modules/repo/repo.module'; GitProviderModule, RepoModule, SynchronizeModule, + NotificationsModule, ], + providers: [InitService], }) export class AppModule implements NestModule { + constructor(private readonly initService: InitService) {} + onModuleInit() { + console.log(`Initialization...`); + this.initService.createUser().then(() => console.log('Initialized !')); + } configure(consumer: MiddlewareConsumer) { consumer.apply(AuthMiddleware).forRoutes('*'); } diff --git a/api/src/auth.middleware.ts b/api/src/auth.middleware.ts index a58cc5d..e049db9 100644 --- a/api/src/auth.middleware.ts +++ b/api/src/auth.middleware.ts @@ -1,9 +1,6 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; -import { verify } from 'jsonwebtoken'; -import { SESSION_COOKIE_NAME } from './common/constants'; import { GitProviderService } from './modules/git-provider/gitprovider.service'; -import { RepoService } from './modules/repo/repo.service'; import { UsersService } from './modules/users/users.service'; @Injectable() @@ -11,30 +8,14 @@ export class AuthMiddleware implements NestMiddleware { constructor( private readonly usersService: UsersService, private readonly gitProviderService: GitProviderService, - private readonly repoService: RepoService, ) {} - async use(req: Request, res: Response, next: NextFunction) { - let token = req.cookies[SESSION_COOKIE_NAME]; - if (!token) { - const authorization = req.header('authorization'); - token = authorization?.replace('Bearer ', ''); - } - - if (!token) { - res.sendStatus(401); - return; - } - - const jwt = verify(token, process.env.JWT_SECRET); - - const user = await this.usersService.findOne(jwt.sub as string); - req['token'] = jwt; + async use(req: Request, _res: Response, next: NextFunction) { + const user = await this.usersService.findOne('user'); req['user'] = user; if (user) { this.gitProviderService.auth(user.gitProviderName, user.gitProviderToken); - this.repoService.auth(user.gitProviderName, user.gitProviderToken); } next(); } diff --git a/api/src/common/constants.ts b/api/src/common/constants.ts deleted file mode 100644 index 0ee6f33..0000000 --- a/api/src/common/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SESSION_COOKIE_NAME = 'next-auth.session-token'; diff --git a/api/src/common/types.ts b/api/src/common/types.ts new file mode 100644 index 0000000..8e1159c --- /dev/null +++ b/api/src/common/types.ts @@ -0,0 +1,5 @@ +export interface Organization { + id: string; + login: string; + full_path?: string; +} diff --git a/api/src/entities/repo.entity.ts b/api/src/entities/repo.entity.ts index dd325ba..4a6eb70 100644 --- a/api/src/entities/repo.entity.ts +++ b/api/src/entities/repo.entity.ts @@ -21,6 +21,12 @@ export class Repo { }) organization?: string; + @Column({ + type: String, + nullable: true, + }) + owner: string; + @OneToMany(() => Commit, (commit) => commit.repo, { cascade: ['insert', 'update'], }) diff --git a/api/src/entities/user.entity.ts b/api/src/entities/user.entity.ts index 5669fb9..6baf2ee 100644 --- a/api/src/entities/user.entity.ts +++ b/api/src/entities/user.entity.ts @@ -1,8 +1,15 @@ -import { Entity, Column, PrimaryColumn, ManyToMany, JoinTable } from 'typeorm'; +import { + Entity, + Column, + PrimaryColumn, + ManyToMany, + JoinTable, + BaseEntity, +} from 'typeorm'; import { Repo } from './repo.entity'; @Entity() -export class User { +export class User extends BaseEntity { @PrimaryColumn() id: string; @@ -48,9 +55,6 @@ export class User { }) gitProviderName: string | null; - @Column({ default: true }) - isActive: boolean; - @ManyToMany(() => Repo, (repo) => repo.users, { cascade: ['insert', 'update'], }) diff --git a/api/src/generated/graphql.ts b/api/src/generated/graphql.ts index 1da7e2e..4bb4774 100644 --- a/api/src/generated/graphql.ts +++ b/api/src/generated/graphql.ts @@ -38615,35 +38615,28 @@ export type DirectiveResolvers = { }; -export const GetAllCommitsOfAllReposOfAllOrgWithPagination = gql` - query GetAllCommitsOfAllReposOfAllOrgWithPagination($orgLogin: String!, $name: String!, $date: GitTimestamp) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - defaultBranchRef { - target { - ... on Commit { - history(since: $date) { - edges { - node { - ... on Commit { - author { - date - email - name - } - id - committedDate - changedFilesIfAvailable - additions - deletions - } +export const GetAllCommitsOfRepository = gql` + query GetAllCommitsOfRepository($name: String!, $owner: String!, $date: GitTimestamp) { + repository(name: $name, owner: $owner) { + id + name + defaultBranchRef { + target { + ... on Commit { + history(since: $date) { + edges { + node { + ... on Commit { + author { + date + email + name } + id + committedDate + changedFilesIfAvailable + additions + deletions } } } @@ -38654,30 +38647,23 @@ export const GetAllCommitsOfAllReposOfAllOrgWithPagination = gql` } } `; -export const GetAllIssuesOfAllReposOfAllOrgWithPagination = gql` - query GetAllIssuesOfAllReposOfAllOrgWithPagination($orgLogin: String!, $name: String!, $cursorIssue: String, $date: DateTime) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - issues(first: 100, after: $cursorIssue, filterBy: {since: $date}) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - state - closedAt - createdAt - } - } +export const GetAllIssuesOfRepoWithPagination = gql` + query GetAllIssuesOfRepoWithPagination($owner: String!, $name: String!, $cursorIssue: String, $date: DateTime) { + repository(name: $name, owner: $owner) { + id + name + issues(first: 100, after: $cursorIssue, filterBy: {since: $date}) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + state + closedAt + createdAt } } } @@ -38698,37 +38684,29 @@ export const GetAllOrgsWithPagination = gql` node { id login - databaseId } } } } } `; -export const GetAllPullRequestsOfAllReposOfAllOrgWithPagination = gql` - query GetAllPullRequestsOfAllReposOfAllOrgWithPagination($orgLogin: String!, $name: String!, $cursorPullRequest: String) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - pullRequests(first: 100, after: $cursorPullRequest) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - state - closedAt - createdAt - } - } +export const GetAllPullRequestsOfRepoWithPagination = gql` + query GetAllPullRequestsOfRepoWithPagination($owner: String!, $name: String!, $cursorPullRequest: String) { + repository(name: $name, owner: $owner) { + id + name + pullRequests(first: 100, after: $cursorPullRequest) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + state + closedAt + createdAt } } } @@ -38736,21 +38714,18 @@ export const GetAllPullRequestsOfAllReposOfAllOrgWithPagination = gql` } `; export const GetAllReposOfOrgWithPagination = gql` - query GetAllReposOfOrgWithPagination($orgLogin: String!, $cursorRepo: String) { - viewer { - login - organization(login: $orgLogin) { - repositories(first: 100, after: $cursorRepo) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - name - } + query GetAllReposOfOrgWithPagination($orgLogin: String!, $privacy: RepositoryPrivacy, $cursorRepo: String) { + organization(login: $orgLogin) { + repositories(first: 100, after: $cursorRepo, privacy: $privacy) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + name } } } @@ -38889,54 +38864,59 @@ export const GetAllReposOfUserWithPagination = gql` id name isInOrganization + owner { + id + login + } } } } } } `; -export type GetAllCommitsOfAllReposOfAllOrgWithPaginationQueryVariables = Exact<{ - orgLogin: Scalars['String']; +export type GetAllCommitsOfRepositoryQueryVariables = Exact<{ name: Scalars['String']; + owner: Scalars['String']; date?: InputMaybe; }>; -export type GetAllCommitsOfAllReposOfAllOrgWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organization?: { __typename?: 'Organization', id: string, login: string, repository?: { __typename?: 'Repository', id: string, name: string, defaultBranchRef?: { __typename?: 'Ref', target?: { __typename?: 'Blob' } | { __typename?: 'Commit', history: { __typename?: 'CommitHistoryConnection', edges?: Array<{ __typename?: 'CommitEdge', node?: { __typename?: 'Commit', id: string, committedDate: any, changedFilesIfAvailable?: number | null, additions: number, deletions: number, author?: { __typename?: 'GitActor', date?: any | null, email?: string | null, name?: string | null } | null } | null } | null> | null } } | { __typename?: 'Tag' } | { __typename?: 'Tree' } | null } | null } | null } | null } }; +export type GetAllCommitsOfRepositoryQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', id: string, name: string, defaultBranchRef?: { __typename?: 'Ref', target?: { __typename?: 'Blob' } | { __typename?: 'Commit', history: { __typename?: 'CommitHistoryConnection', edges?: Array<{ __typename?: 'CommitEdge', node?: { __typename?: 'Commit', id: string, committedDate: any, changedFilesIfAvailable?: number | null, additions: number, deletions: number, author?: { __typename?: 'GitActor', date?: any | null, email?: string | null, name?: string | null } | null } | null } | null> | null } } | { __typename?: 'Tag' } | { __typename?: 'Tree' } | null } | null } | null }; -export type GetAllIssuesOfAllReposOfAllOrgWithPaginationQueryVariables = Exact<{ - orgLogin: Scalars['String']; +export type GetAllIssuesOfRepoWithPaginationQueryVariables = Exact<{ + owner: Scalars['String']; name: Scalars['String']; cursorIssue?: InputMaybe; date?: InputMaybe; }>; -export type GetAllIssuesOfAllReposOfAllOrgWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organization?: { __typename?: 'Organization', id: string, login: string, repository?: { __typename?: 'Repository', id: string, name: string, issues: { __typename?: 'IssueConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'IssueEdge', cursor: string, node?: { __typename?: 'Issue', id: string, state: IssueState, closedAt?: any | null, createdAt: any } | null } | null> | null } } | null } | null } }; +export type GetAllIssuesOfRepoWithPaginationQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', id: string, name: string, issues: { __typename?: 'IssueConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'IssueEdge', cursor: string, node?: { __typename?: 'Issue', id: string, state: IssueState, closedAt?: any | null, createdAt: any } | null } | null> | null } } | null }; export type GetAllOrgsWithPaginationQueryVariables = Exact<{ cursorOrgs?: InputMaybe; }>; -export type GetAllOrgsWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organizations: { __typename?: 'OrganizationConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'OrganizationEdge', cursor: string, node?: { __typename?: 'Organization', id: string, login: string, databaseId?: number | null } | null } | null> | null } } }; +export type GetAllOrgsWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organizations: { __typename?: 'OrganizationConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'OrganizationEdge', cursor: string, node?: { __typename?: 'Organization', id: string, login: string } | null } | null> | null } } }; -export type GetAllPullRequestsOfAllReposOfAllOrgWithPaginationQueryVariables = Exact<{ - orgLogin: Scalars['String']; +export type GetAllPullRequestsOfRepoWithPaginationQueryVariables = Exact<{ + owner: Scalars['String']; name: Scalars['String']; cursorPullRequest?: InputMaybe; }>; -export type GetAllPullRequestsOfAllReposOfAllOrgWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organization?: { __typename?: 'Organization', id: string, login: string, repository?: { __typename?: 'Repository', id: string, name: string, pullRequests: { __typename?: 'PullRequestConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'PullRequestEdge', cursor: string, node?: { __typename?: 'PullRequest', id: string, state: PullRequestState, closedAt?: any | null, createdAt: any } | null } | null> | null } } | null } | null } }; +export type GetAllPullRequestsOfRepoWithPaginationQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', id: string, name: string, pullRequests: { __typename?: 'PullRequestConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'PullRequestEdge', cursor: string, node?: { __typename?: 'PullRequest', id: string, state: PullRequestState, closedAt?: any | null, createdAt: any } | null } | null> | null } } | null }; export type GetAllReposOfOrgWithPaginationQueryVariables = Exact<{ orgLogin: Scalars['String']; + privacy?: InputMaybe; cursorRepo?: InputMaybe; }>; -export type GetAllReposOfOrgWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, organization?: { __typename?: 'Organization', repositories: { __typename?: 'RepositoryConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'RepositoryEdge', cursor: string, node?: { __typename?: 'Repository', id: string, name: string } | null } | null> | null } } | null } }; +export type GetAllReposOfOrgWithPaginationQuery = { __typename?: 'Query', organization?: { __typename?: 'Organization', repositories: { __typename?: 'RepositoryConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'RepositoryEdge', cursor: string, node?: { __typename?: 'Repository', id: string, name: string } | null } | null> | null } } | null }; export type GetAllCommitsOfAllReposOfUserWithPaginationQueryVariables = Exact<{ cursorRepo?: InputMaybe; @@ -38968,4 +38948,4 @@ export type GetAllReposOfUserWithPaginationQueryVariables = Exact<{ }>; -export type GetAllReposOfUserWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, repositories: { __typename?: 'RepositoryConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'RepositoryEdge', cursor: string, node?: { __typename?: 'Repository', id: string, name: string, isInOrganization: boolean } | null } | null> | null } } }; +export type GetAllReposOfUserWithPaginationQuery = { __typename?: 'Query', viewer: { __typename?: 'User', login: string, repositories: { __typename?: 'RepositoryConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, edges?: Array<{ __typename?: 'RepositoryEdge', cursor: string, node?: { __typename?: 'Repository', id: string, name: string, isInOrganization: boolean, owner: { __typename?: 'Organization', id: string, login: string } | { __typename?: 'User', id: string, login: string } } | null } | null> | null } } }; diff --git a/api/src/init.service.ts b/api/src/init.service.ts new file mode 100644 index 0000000..a0c4f6b --- /dev/null +++ b/api/src/init.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Repo } from './entities/repo.entity'; +import { GitProviderService } from './modules/git-provider/gitprovider.service'; +import { UsersService } from './modules/users/users.service'; + +@Injectable() +export class InitService { + constructor( + private readonly usersService: UsersService, + private readonly gitProviderService: GitProviderService, + ) {} + + async createUser() { + await this.usersService.upsert('user', { + email: 'user@email.com', + name: 'user', + gitProviderToken: process.env.GIT_PROVIDER_TOKEN, + gitProviderName: process.env.GIT_PROVIDER_NAME, + }); + const user = await this.usersService.findOne('user'); + this.gitProviderService.auth(user.gitProviderName, user.gitProviderToken); + const orgs = await this.gitProviderService.getAllOrgs(); + const reposOrg = await this.gitProviderService.getAllRepoOfOrgs(orgs); + const reposUser = await this.gitProviderService.getAllReposOfUser(); + const reposEntities: Repo[] = reposOrg.concat(reposUser); + await this.usersService.addRepositories(user, reposEntities); + } +} diff --git a/api/src/modules/git-provider/github.service.ts b/api/src/modules/git-provider/github.service.ts new file mode 100644 index 0000000..19e6f42 --- /dev/null +++ b/api/src/modules/git-provider/github.service.ts @@ -0,0 +1,372 @@ +import { Injectable } from '@nestjs/common'; +import { Octokit } from '@octokit/rest'; +import { ApolloService } from '../apollo-client/apollo.service'; +import { + GetAllReposOfOrgWithPaginationQuery, + GetAllReposOfOrgWithPagination, + GetAllOrgsWithPaginationQuery, + GetAllOrgsWithPagination, + GetAllPullRequestsOfRepoWithPaginationQuery, + GetAllPullRequestsOfRepoWithPagination, + GetAllIssuesOfRepoWithPaginationQuery, + GetAllIssuesOfRepoWithPagination, + GetAllCommitsOfRepositoryQuery, + GetAllCommitsOfRepository, + GetAllReposOfUserWithPaginationQuery, + GetAllReposOfUserWithPagination, +} from 'src/generated/graphql'; +import { ApolloQueryResult } from '@apollo/client'; +import { Repo } from 'src/entities/repo.entity'; +import { IGitProvider } from './gitprovider.service'; +import { Organization } from 'src/common/types'; +import { Commit } from 'src/entities/commit.entity'; +import { Issue } from 'src/entities/issue.entity'; +import { PullRequest } from 'src/entities/pullrequest.entity'; + +@Injectable() +export class GithubService implements IGitProvider { + apolloService: ApolloService; + #octokit: Octokit; + #token: string; + + auth(token: string): void { + this.#token = token; + this.#octokit = new Octokit({ + auth: token, + }); + this.apolloService = new ApolloService(token); + } + + getToken() { + return this.#token; + } + + async getAllOrganizations(): Promise { + const organizations: { id: number; login: string }[] = []; + let orgEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllOrgsWithPagination, + variables: { + cursorOrg: orgEndCursor, + }, + }); + + orgEndCursor = + graphQLResultWithPagination.data.viewer.organizations.pageInfo + .endCursor; + + graphQLResultWithPagination.data.viewer.organizations.edges.map( + (o: Record) => { + organizations.push({ + id: o.node.id, + login: o.node.login, + }); + }, + ); + } while ( + graphQLResultWithPagination.data.viewer.organizations.pageInfo.hasNextPage + ); + + return organizations.flatMap((o) => o.login); + } + + async getProfile(): Promise<{ id: number; login: string }> { + return (await this.#octokit.rest.users.getAuthenticated()).data; + } + + async getOrgRepositories(org: string, onlyPublic: boolean): Promise { + const repositories: Repo[] = []; + let repoEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllReposOfOrgWithPagination, + variables: { + privacy: onlyPublic ? 'PUBLIC' : null, + orgLogin: org, + cursorRepo: repoEndCursor, + }, + }); + + repoEndCursor = + graphQLResultWithPagination.data.organization.repositories.pageInfo + .endCursor; + + graphQLResultWithPagination.data.organization.repositories.edges.map( + (r: Record) => { + const repo: Repo = new Repo(); + repo.id = r.node.id; + repo.name = r.node.name; + repo.organization = org; + repo.owner = org; + + repositories.push(repo); + }, + ); + } while ( + graphQLResultWithPagination.data.organization.repositories.pageInfo + .hasNextPage + ); + + return repositories; + } + + // Get all organisations + async getAllOrgs(): Promise { + const organizations: Organization[] = []; + let orgEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllOrgsWithPagination, + variables: { + cursorOrg: orgEndCursor, + }, + }); + + orgEndCursor = + graphQLResultWithPagination.data.viewer.organizations.pageInfo + .endCursor; + + graphQLResultWithPagination.data.viewer.organizations.edges.map( + (o: Record) => { + organizations.push({ + id: o.node.id, + login: o.node.login, + }); + }, + ); + } while ( + graphQLResultWithPagination.data.viewer.organizations.pageInfo.hasNextPage + ); + + return organizations; + } + + // Get all repositories + async getAllReposOfOrgs(orgs: Organization[]): Promise { + const repositories: Repo[] = []; + let repoEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + await Promise.all([ + ...orgs.map(async (org) => { + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllReposOfOrgWithPagination, + variables: { + orgLogin: org.login, + cursorRepo: repoEndCursor, + }, + }); + + repoEndCursor = + graphQLResultWithPagination.data.organization.repositories.pageInfo + .endCursor; + + graphQLResultWithPagination.data.organization.repositories.edges.map( + (r: Record) => { + const repo: Repo = new Repo(); + repo.id = r.node.id; + repo.name = r.node.name; + repo.organization = org.login; + repo.owner = org.login; + + repositories.push(repo); + }, + ); + } while ( + graphQLResultWithPagination.data.organization.repositories.pageInfo + .hasNextPage + ); + }), + ]); + return repositories; + } + + async getAllReposOfUser(): Promise { + const repositories: Repo[] = []; + let repoEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllReposOfUserWithPagination, + variables: { + cursorRepo: repoEndCursor, + }, + }); + + repoEndCursor = + graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; + + graphQLResultWithPagination.data.viewer.repositories.edges + .filter((r: Record) => !r.node.isInOrganization) + .map((r: Record) => { + const repo: Repo = new Repo(); + repo.id = r.node.id; + repo.name = r.node.name; + repo.owner = r.node.owner?.login; + repositories.push(repo); + }); + } while ( + graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage + ); + + return repositories; + } + + // Get all commits + async getCommitsOfRepos(repos: Repo[], date: Date): Promise { + return ( + await Promise.all([ + ...repos.map(async (repo) => { + const graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllCommitsOfRepository, + variables: { + name: repo.name, + owner: repo.owner, + date, + }, + }); + + if ( + graphQLResultWithPagination.data.repository.defaultBranchRef?.target + .__typename === 'Commit' + ) { + return graphQLResultWithPagination.data.repository.defaultBranchRef.target.history.edges.map( + (c) => { + const commit = new Commit(); + commit.id = c.node.id; + commit.repoId = repo.id; + commit.author = c.node.author.name; + commit.date = c.node.committedDate; + commit.numberOfLineAdded = c.node.additions; + commit.numberOfLineRemoved = c.node.deletions; + commit.totalNumberOfLine = c.node.additions + c.node.deletions; + return commit; + }, + ); + } + return []; + }), + ]) + ).flatMap((c) => c); + } + + // Get all issues + async getIssuesOfRepos(repos: Repo[], date: Date): Promise { + let issueEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + return ( + await Promise.all([ + ...repos.map(async (r) => { + const issues: Issue[] = []; + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllIssuesOfRepoWithPagination, + variables: { + owner: r.owner, + name: r.name, + cursorIssue: issueEndCursor, + date, + }, + }); + + issueEndCursor = + graphQLResultWithPagination.data.repository.issues.pageInfo + .endCursor; + issues.push( + ...graphQLResultWithPagination.data.repository.issues.edges.map( + (i) => { + const issue: Issue = new Issue(); + issue.id = i.node.id; + issue.repoId = r.id; + issue.state = i.node.state; + issue.createdAt = i.node.createdAt; + issue.closedAt = i.node.closedAt; + + return issue; + }, + ), + ); + } while ( + graphQLResultWithPagination.data.repository.issues.pageInfo + .hasNextPage + ); + return issues; + }), + ]) + ).flatMap((i) => i); + } + + // Get all pull requests + async getPullRequestsOfRepos( + repos: Repo[], + date: Date, + ): Promise { + let pullRequestEndCursor: string = null; + let graphQLResultWithPagination: ApolloQueryResult; + + return ( + await Promise.all([ + ...repos.map(async (r) => { + const pullRequests: PullRequest[] = []; + do { + graphQLResultWithPagination = await this.apolloService + .githubClient() + .query({ + query: GetAllPullRequestsOfRepoWithPagination, + variables: { + owner: r.owner, + name: r.name, + cursorPullRequest: pullRequestEndCursor, + }, + }); + + pullRequestEndCursor = + graphQLResultWithPagination.data.repository.pullRequests.pageInfo + .endCursor; + pullRequests.push( + ...graphQLResultWithPagination.data.repository.pullRequests.edges.map( + (p) => { + const pullRequest: PullRequest = new PullRequest(); + pullRequest.id = p.node.id; + pullRequest.repoId = r.id; + pullRequest.state = p.node.state; + pullRequest.createdAt = p.node.createdAt; + pullRequest.closedAt = p.node.closedAt; + + return pullRequest; + }, + ), + ); + } while ( + graphQLResultWithPagination.data.repository.pullRequests.pageInfo + .hasNextPage + ); + return pullRequests; + }), + ]) + ).flatMap((pr) => pr); + } +} diff --git a/api/src/modules/repo/gitlab/repo.gitlab.service.ts b/api/src/modules/git-provider/gitlab.service.ts similarity index 78% rename from api/src/modules/repo/gitlab/repo.gitlab.service.ts rename to api/src/modules/git-provider/gitlab.service.ts index dc4b8ee..7463f35 100644 --- a/api/src/modules/repo/gitlab/repo.gitlab.service.ts +++ b/api/src/modules/git-provider/gitlab.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { ApolloService } from '../apollo-client/apollo.service'; import { Repo } from 'src/entities/repo.entity'; - import { Gitlab } from '@gitbeaker/node'; import { ProjectSchema, @@ -8,39 +8,57 @@ import { IssueSchema, MergeRequestSchema, } from '@gitbeaker/core/dist/types/types'; +import { PullRequest } from 'src/entities/pullrequest.entity'; import { Commit } from 'src/entities/commit.entity'; import { Issue } from 'src/entities/issue.entity'; -import { PullRequest } from 'src/entities/pullrequest.entity'; - -export interface Organization { - id: string; - login: string; - databaseId: number; -} +import { Organization } from 'src/common/types'; const defaultAPI = new Gitlab(null); @Injectable() -export class RepoGitlabService { +export class GitlabService { + apolloService: ApolloService; + #token: string; #api: typeof defaultAPI; + constructor() {} + auth(token: string): void { + this.#token = token; this.#api = new Gitlab({ host: 'https://gitlab.com', oauthToken: token, }); } + getToken() { + return this.#token; + } + + async getAllOrganizations(): Promise { + const orgs = await this.#api.Groups.all({ maxPages: 50000 }); + return orgs.flatMap((o) => o.full_path); + } + + async getProfile(): Promise<{ id: number; login: string }> { + const { id, username: login } = await this.#api.Users.current(); + return { id, login }; + } + + async getOrgRepositories(org: string): Promise { + const repositories: Repo[] = []; + return repositories; + } + // Get all organisations - async getAllOrgWithPagination(): Promise { + async getAllOrgs(): Promise { const organizations: Organization[] = []; return organizations; } // Get all repositories - async getAllRepoOfAllOrgWithPagination(): Promise { + async getAllReposOfOrgs(orgs: Organization[]): Promise { const repositories: Repo[] = []; - const orgs = await this.#api.Groups.all({ maxPages: 50000 }); await Promise.all([ ...orgs.map(async (o) => { @@ -67,21 +85,17 @@ export class RepoGitlabService { return repositories; } - async getAllRepoOfUserWithPagination(): Promise { + async getAllReposOfUser(): Promise { const repositories: Repo[] = []; return repositories; } // Get all commits - async getCommitsOfAllRepoOfAllOrgWithPagination( - date: Date, - ): Promise { - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - + async getCommitsOfRepos(repos: Repo[], date: Date): Promise { return ( await Promise.all([ - ...allRepos + ...repos .filter((r) => r.id) .map(async (r) => { const commitsGitlab = await this.#api.Commits.all(r.id, { @@ -108,18 +122,11 @@ export class RepoGitlabService { ).flatMap((c) => c); } - async getCommitsOfAllRepoOfUserWithPagination(date: Date): Promise { - const repositories: Commit[] = []; - return repositories; - } - // Get all issues - async getIssuesOfAllRepoOfAllOrgWithPagination(date: Date): Promise { - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - + async getIssuesOfRepos(repos: Repo[], date: Date): Promise { return ( await Promise.all([ - ...allRepos + ...repos .filter((r) => r.id) .map(async (r) => { const issuesGitlab = await this.#api.Issues.all({ @@ -144,20 +151,14 @@ export class RepoGitlabService { ).flatMap((i) => i); } - async getIssuesOfAllRepoOfUserWithPagination(date: Date): Promise { - const issues: Issue[] = []; - return issues; - } - // Get all pull requests - async getPullRequestsOfAllRepoOfAllOrgWithPagination( + async getPullRequestsOfRepos( + repos: Repo[], date: Date, ): Promise { - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - return ( await Promise.all([ - ...allRepos + ...repos .filter((r) => r.id) .map(async (r) => { const mergeRequestsGitlab = await this.#api.MergeRequests.all({ @@ -181,9 +182,4 @@ export class RepoGitlabService { ]) ).flatMap((pr) => pr); } - - async getPullRequestsOfAllRepoOfUserWithPagination(): Promise { - const pullRequests: PullRequest[] = []; - return pullRequests; - } } diff --git a/api/src/modules/git-provider/gitprovider.module.ts b/api/src/modules/git-provider/gitprovider.module.ts index 8aadadf..1e29296 100644 --- a/api/src/modules/git-provider/gitprovider.module.ts +++ b/api/src/modules/git-provider/gitprovider.module.ts @@ -1,13 +1,11 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { GithubModule } from '../github/github.module'; -import { GithubService } from '../github/github.service'; -import { GitlabModule } from '../gitlab/gitlab.module'; -import { GitlabService } from '../gitlab/gitlab.service'; +import { GithubService } from './github.service'; +import { GitlabService } from './gitlab.service'; import { GitProviderService } from './gitprovider.service'; @Module({ - imports: [GithubModule, GitlabModule, HttpModule], + imports: [HttpModule], exports: [GitProviderService], providers: [GitProviderService, GithubService, GitlabService], controllers: [], diff --git a/api/src/modules/git-provider/gitprovider.service.ts b/api/src/modules/git-provider/gitprovider.service.ts index 2db6a60..30e50a0 100644 --- a/api/src/modules/git-provider/gitprovider.service.ts +++ b/api/src/modules/git-provider/gitprovider.service.ts @@ -1,55 +1,75 @@ import { Injectable } from '@nestjs/common'; import { Repo } from 'src/entities/repo.entity'; -import { GithubService } from '../github/github.service'; -import { GitlabService } from '../gitlab/gitlab.service'; -import { HttpService } from '@nestjs/axios'; +import { GithubService } from './github.service'; +import { GitlabService } from './gitlab.service'; +import { Organization } from 'src/common/types'; +import { Commit } from 'src/entities/commit.entity'; +import { Issue } from 'src/entities/issue.entity'; +import { PullRequest } from 'src/entities/pullrequest.entity'; export interface IGitProvider { auth(token: string): void; - getAllOrganizations(): Promise; getProfile(): Promise<{ id: number; login: string }>; - getRepositories(): Promise; - getOrgRepositories(org: string): Promise; - revokeAccess(token: string): void; + getOrgRepositories(org: string, onlyPublic: boolean): Promise; + + getAllOrgs(): Promise; + getAllReposOfOrgs(orgs: Organization[]): Promise; + getAllReposOfUser(): Promise; + getCommitsOfRepos(repos: Repo[], date: Date): Promise; + getIssuesOfRepos(repos: Repo[], date: Date): Promise; + getPullRequestsOfRepos(repos: Repo[], date: Date): Promise; } @Injectable() export class GitProviderService { #gitProvider: IGitProvider; - #token: string; - constructor(private readonly httpService: HttpService) {} + constructor() {} auth(providerName: string, token: string): void { - this.#token = token; if (providerName === 'github') { this.#gitProvider = new GithubService(); } else if (providerName === 'gitlab') { - this.#gitProvider = new GitlabService(this.httpService); + this.#gitProvider = new GitlabService(); } this.#gitProvider.auth(token); } - getToken() { - return this.#token; + async getProfile(): Promise<{ id: number; login: string }> { + return await this.#gitProvider.getProfile(); } - async getAllOrganizations(): Promise { - return await this.#gitProvider.getAllOrganizations(); + async getAllOrgs(): Promise { + return await this.#gitProvider.getAllOrgs(); } - async getProfile(): Promise<{ id: number; login: string }> { - return await this.#gitProvider.getProfile(); + async getOrgRepositories(org: string, onlyPublic: boolean): Promise { + return await this.#gitProvider.getOrgRepositories(org, onlyPublic); + } + + // Get all repositories + async getAllRepoOfOrgs(orgs: Organization[]): Promise { + return await this.#gitProvider.getAllReposOfOrgs(orgs); + } + + async getAllReposOfUser(): Promise { + return await this.#gitProvider.getAllReposOfUser(); } - async getRepositories(): Promise { - return await this.#gitProvider.getRepositories(); + // Get all commits + async getCommitsOfRepos(repos: Repo[], date: Date): Promise { + return await this.#gitProvider.getCommitsOfRepos(repos, date); } - async getOrgRepositories(org: string): Promise { - return await this.#gitProvider.getOrgRepositories(org); + // Get all issues + async getIssuesOfRepos(repos: Repo[], date: Date): Promise { + return await this.#gitProvider.getIssuesOfRepos(repos, date); } - async revokeAccess(token: string): Promise { - return this.#gitProvider.revokeAccess(token); + // Get all pull requests + async getPullRequestsOfRepos( + repos: Repo[], + date: Date, + ): Promise { + return await this.#gitProvider.getPullRequestsOfRepos(repos, date); } } diff --git a/api/src/modules/github/github.module.ts b/api/src/modules/github/github.module.ts deleted file mode 100644 index f3f2db6..0000000 --- a/api/src/modules/github/github.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GithubService } from './github.service'; - -@Module({ - imports: [], - exports: [GithubService], - providers: [GithubService], - controllers: [], -}) -export class GithubModule {} diff --git a/api/src/modules/github/github.service.ts b/api/src/modules/github/github.service.ts deleted file mode 100644 index 9753e18..0000000 --- a/api/src/modules/github/github.service.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Octokit } from '@octokit/rest'; -import { createOAuthAppAuth } from '@octokit/auth-oauth-app'; -import { ApolloService } from '../apollo-client/apollo.service'; -import { - GetAllReposOfOrgWithPaginationQuery, - GetAllReposOfOrgWithPagination, - GetAllOrgsWithPaginationQuery, - GetAllOrgsWithPagination, - GetAllReposOfUserWithPaginationQuery, - GetAllReposOfUserWithPagination, -} from 'src/generated/graphql'; -import { ApolloQueryResult } from '@apollo/client'; -import { Repo } from 'src/entities/repo.entity'; - -export type GithubIssue = { - id: number; - node_id: string; - state: string; - created_at: string; - closed_at: string; - repository_url: string; - comments?: number; - org?: string; -}; - -@Injectable() -export class GithubService { - apolloService: ApolloService; - #octokit: Octokit; - #token: string; - - auth(token: string): void { - this.#token = token; - this.#octokit = new Octokit({ - auth: token, - }); - this.apolloService = new ApolloService(token); - } - - getToken() { - return this.#token; - } - - async getAllOrganizations(): Promise { - const organizations: { id: number; login: string }[] = []; - let orgEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllOrgsWithPagination, - variables: { - cursorOrg: orgEndCursor, - }, - }); - - orgEndCursor = - graphQLResultWithPagination.data.viewer.organizations.pageInfo - .endCursor; - - graphQLResultWithPagination.data.viewer.organizations.edges.map( - (o: Record) => { - organizations.push({ - id: o.node.id, - login: o.node.login, - }); - }, - ); - } while ( - graphQLResultWithPagination.data.viewer.organizations.pageInfo.hasNextPage - ); - - return organizations.flatMap((o) => o.login); - } - - async getProfile(): Promise<{ id: number; login: string }> { - return (await this.#octokit.rest.users.getAuthenticated()).data; - } - - async getRepositories(): Promise { - const repositories: Repo[] = []; - let repoEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllReposOfUserWithPagination, - variables: { - cursorRepo: repoEndCursor, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; - - graphQLResultWithPagination.data.viewer.repositories.edges - .filter((r: Record) => !r.node.isInOrganization) - .map((r: Record) => { - const repo: Repo = new Repo(); - repo.id = r.node.id; - repo.name = r.node.name; - repositories.push(repo); - }); - } while ( - graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage - ); - - return repositories; - } - - async getOrgRepositories(org: string): Promise { - const repositories: Repo[] = []; - let repoEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllReposOfOrgWithPagination, - variables: { - orgLogin: org, - cursorRepo: repoEndCursor, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.organization.repositories - .pageInfo.endCursor; - - graphQLResultWithPagination.data.viewer.organization.repositories.edges.map( - (r: Record) => { - const repo: Repo = new Repo(); - repo.id = r.node.id; - repo.name = r.node.name; - repo.organization = org; - - repositories.push(repo); - }, - ); - } while ( - graphQLResultWithPagination.data.viewer.organization.repositories.pageInfo - .hasNextPage - ); - - return repositories; - } - - async revokeAccess(token: string): Promise { - const appOctokit = new Octokit({ - authStrategy: createOAuthAppAuth, - auth: { - clientId: process.env.GITHUB_ID, - clientSecret: process.env.GITHUB_SECRET, - }, - }); - - await appOctokit.rest.apps.deleteAuthorization({ - client_id: process.env.GITHUB_ID, - access_token: token, - }); - } -} diff --git a/api/src/modules/gitlab/gitlab.module.ts b/api/src/modules/gitlab/gitlab.module.ts deleted file mode 100644 index f7bfe88..0000000 --- a/api/src/modules/gitlab/gitlab.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; -import { GitlabService } from './gitlab.service'; - -@Module({ - imports: [HttpModule], - exports: [GitlabService], - providers: [GitlabService], - controllers: [], -}) -export class GitlabModule {} diff --git a/api/src/modules/gitlab/gitlab.service.ts b/api/src/modules/gitlab/gitlab.service.ts deleted file mode 100644 index fde683e..0000000 --- a/api/src/modules/gitlab/gitlab.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ApolloService } from '../apollo-client/apollo.service'; -import { Repo } from 'src/entities/repo.entity'; -import { Gitlab } from '@gitbeaker/node'; -import { HttpService } from '@nestjs/axios'; - -export type GithubIssue = { - id: number; - node_id: string; - state: string; - created_at: string; - closed_at: string; - repository_url: string; - comments?: number; - org?: string; -}; - -const defaultAPI = new Gitlab(null); - -@Injectable() -export class GitlabService { - apolloService: ApolloService; - #token: string; - #api: typeof defaultAPI; - - constructor(private readonly httpService: HttpService) {} - - auth(token: string): void { - this.#token = token; - this.#api = new Gitlab({ - host: 'https://gitlab.com', - oauthToken: token, - }); - } - - getToken() { - return this.#token; - } - - async getAllOrganizations(): Promise { - const orgs = await this.#api.Groups.all({ maxPages: 50000 }); - return orgs.flatMap((o) => o.full_path); - } - - async getProfile(): Promise<{ id: number; login: string }> { - const { id, username: login } = await this.#api.Users.current(); - return { id, login }; - } - - async getRepositories(): Promise { - const repositories: Repo[] = []; - return repositories; - } - - async getOrgRepositories(org: string): Promise { - const repositories: Repo[] = []; - return repositories; - } - - async revokeAccess(token: string): Promise { - this.httpService.post('https://gitlab.com/oauth/revoke', { - client_id: process.env.GITLAB_ID, - client_secret: process.env.GITLAB_SECRET, - token, - }); - } -} diff --git a/api/src/modules/notifications/notifications.module.ts b/api/src/modules/notifications/notifications.module.ts new file mode 100644 index 0000000..97e8e37 --- /dev/null +++ b/api/src/modules/notifications/notifications.module.ts @@ -0,0 +1,12 @@ +import { HttpModule } from '@nestjs/axios'; +import { Global, Module } from '@nestjs/common'; +import { NotificationsService } from './notifications.service'; + +@Global() +@Module({ + imports: [HttpModule], + exports: [NotificationsService], + providers: [NotificationsService], + controllers: [], +}) +export class NotificationsModule {} diff --git a/api/src/modules/notifications/notifications.service.ts b/api/src/modules/notifications/notifications.service.ts new file mode 100644 index 0000000..52eb017 --- /dev/null +++ b/api/src/modules/notifications/notifications.service.ts @@ -0,0 +1,61 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; +import { User } from 'src/entities/user.entity'; +import { UserDTO } from '../users/users.dto'; + +export interface Notification { + event: + | 'new_user' + | 'checkout_pro' + | 'subscribed_pro' + | 'canceled_pro' + | 'synchronize_job_created' + | 'synchronize_job_ended' + | 'synchronize_job_error' + | 'synchronize_public_job_created' + | 'synchronize_public_job_ended' + | 'synchronize_public_job_error'; + data: Record; +} + +const userKeysToExclude = ['repos', 'gitProviderId', 'gitProviderToken']; + +@Injectable() +export class NotificationsService { + #notificationWebhookUrl: string; + #currentUser: UserDTO; + + constructor(private readonly httpService: HttpService) { + this.#notificationWebhookUrl = process.env.NOTIFICATION_WEBHOOK_URL; + } + + setCurrentUser(user: User) { + this.#currentUser = user; + } + + async notify(notification: Notification) { + if (!this.#notificationWebhookUrl) { + console.info('No notification url setup. skipping notification'); + return; + } + await firstValueFrom( + await this.httpService.post(this.#notificationWebhookUrl, { + content: `new event: "${ + notification.event + }" with data: ${JSON.stringify( + { + ...Object.fromEntries( + Object.entries(this.#currentUser || {}).filter( + ([k]) => !userKeysToExclude.includes(k), + ), + ), + ...notification.data, + }, + null, + 4, + )}`, + }), + ); + } +} diff --git a/api/src/modules/repo/github/repo.github.module.ts b/api/src/modules/repo/github/repo.github.module.ts deleted file mode 100644 index 5890c42..0000000 --- a/api/src/modules/repo/github/repo.github.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RepoGithubService } from './repo.github.service'; - -@Module({ - imports: [], - exports: [RepoGithubService], - providers: [RepoGithubService], - controllers: [], -}) -export class RepoGithubModule {} diff --git a/api/src/modules/repo/github/repo.github.service.ts b/api/src/modules/repo/github/repo.github.service.ts deleted file mode 100644 index f3160c3..0000000 --- a/api/src/modules/repo/github/repo.github.service.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Repo } from 'src/entities/repo.entity'; -import { Issue } from 'src/entities/issue.entity'; -import { Commit } from 'src/entities/commit.entity'; -import { - GetAllOrgsWithPagination, - GetAllOrgsWithPaginationQuery, - GetAllReposOfOrgWithPagination, - GetAllReposOfOrgWithPaginationQuery, - GetAllReposOfUserWithPaginationQuery, - GetAllReposOfUserWithPagination, - GetAllCommitsOfAllReposOfAllOrgWithPagination, - GetAllCommitsOfAllReposOfAllOrgWithPaginationQuery, - GetAllCommitsOfAllReposOfUserWithPagination, - GetAllCommitsOfAllReposOfUserWithPaginationQuery, - GetAllIssuesOfAllReposOfAllOrgWithPaginationQuery, - GetAllIssuesOfAllReposOfAllOrgWithPagination, - GetAllPullRequestsOfAllReposOfAllOrgWithPaginationQuery, - GetAllPullRequestsOfAllReposOfAllOrgWithPagination, - GetAllIssuesOfAllRepoOfUserWithPaginationQuery, - GetAllIssuesOfAllRepoOfUserWithPagination, - GetAllPullRequestsOfAllRepoOfUserWithPaginationQuery, - GetAllPullRequestsOfAllRepoOfUserWithPagination, -} from 'src/generated/graphql'; -import { PullRequest } from 'src/entities/pullrequest.entity'; -import { ApolloQueryResult } from '@apollo/client'; -import { ApolloService } from 'src/modules/apollo-client/apollo.service'; - -export interface Organization { - id: string; - login: string; - databaseId: number; -} - -@Injectable() -export class RepoGithubService { - apolloService: ApolloService; - - auth(token: string): void { - this.apolloService = new ApolloService(token); - } - - // Get all organisations - async getAllOrgWithPagination(): Promise { - const organizations: Organization[] = []; - let orgEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllOrgsWithPagination, - variables: { - cursorOrg: orgEndCursor, - }, - }); - - orgEndCursor = - graphQLResultWithPagination.data.viewer.organizations.pageInfo - .endCursor; - - graphQLResultWithPagination.data.viewer.organizations.edges.map( - (o: Record) => { - organizations.push({ - id: o.node.id, - login: o.node.login, - databaseId: o.node.databaseId, - }); - }, - ); - } while ( - graphQLResultWithPagination.data.viewer.organizations.pageInfo.hasNextPage - ); - - return organizations; - } - - // Get all repositories - async getAllRepoOfAllOrgWithPagination(): Promise { - const repositories: Repo[] = []; - let repoEndCursor: string = null; - const allOrgs = await this.getAllOrgWithPagination(); - let graphQLResultWithPagination: ApolloQueryResult; - - await Promise.all([ - ...allOrgs.map(async (o) => { - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllReposOfOrgWithPagination, - variables: { - orgLogin: o.login, - cursorRepo: repoEndCursor, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.organization.repositories - .pageInfo.endCursor; - - graphQLResultWithPagination.data.viewer.organization.repositories.edges.map( - (r: Record) => { - const repo: Repo = new Repo(); - repo.id = r.node.id; - repo.name = r.node.name; - repo.organization = o.login; - - repositories.push(repo); - }, - ); - } while ( - graphQLResultWithPagination.data.viewer.organization.repositories - .pageInfo.hasNextPage - ); - }), - ]); - return repositories; - } - - async getAllRepoOfUserWithPagination(): Promise { - const repositories: Repo[] = []; - let repoEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllReposOfUserWithPagination, - variables: { - cursorRepo: repoEndCursor, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; - - graphQLResultWithPagination.data.viewer.repositories.edges - .filter((r: Record) => !r.node.isInOrganization) - .map((r: Record) => { - const repo: Repo = new Repo(); - repo.id = r.node.id; - repo.name = r.node.name; - repositories.push(repo); - }); - } while ( - graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage - ); - - return repositories; - } - - // Get all commits - async getCommitsOfAllRepoOfAllOrgWithPagination( - date: Date, - ): Promise { - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - - return ( - await Promise.all([ - ...allRepos.map(async (r) => { - const graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllCommitsOfAllReposOfAllOrgWithPagination, - variables: { - orgLogin: r.organization, - name: r.name, - date, - }, - }); - - if ( - graphQLResultWithPagination.data.viewer.organization.repository - .defaultBranchRef?.target.__typename === 'Commit' - ) { - return graphQLResultWithPagination.data.viewer.organization.repository.defaultBranchRef.target.history.edges.map( - (c) => { - const commit = new Commit(); - commit.id = c.node.id; - commit.repoId = r.id; - commit.author = c.node.author.name; - commit.date = c.node.committedDate; - commit.numberOfLineAdded = c.node.additions; - commit.numberOfLineRemoved = c.node.deletions; - commit.totalNumberOfLine = c.node.additions + c.node.deletions; - return commit; - }, - ); - } - return []; - }), - ]) - ).flatMap((c) => c); - } - - async getCommitsOfAllRepoOfUserWithPagination(date: Date): Promise { - let commits: Commit[] = []; - let repoEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllCommitsOfAllReposOfUserWithPagination, - variables: { - cursorRepo: repoEndCursor, - date, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; - - commits = commits.concat( - graphQLResultWithPagination.data.viewer.repositories.edges - .filter((r) => !r.node.isInOrganization) - .flatMap((r) => { - if (r.node.defaultBranchRef?.target.__typename === 'Commit') { - const commits: Commit[] = - r.node.defaultBranchRef.target.history.edges.map( - (c: Record) => { - const commit = new Commit(); - commit.id = c.node.id; - commit.repoId = r.node.id; - commit.author = c.node.author.name; - commit.date = c.node.committedDate; - commit.numberOfLineAdded = c.node.additions; - commit.numberOfLineRemoved = c.node.deletions; - commit.totalNumberOfLine = - c.node.additions + c.node.deletions; - - return commit; - }, - ); - return commits; - } - return []; - }), - ); - } while ( - graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage - ); - - return commits; - } - - // Get all issues - async getIssuesOfAllRepoOfAllOrgWithPagination(date: Date): Promise { - let issueEndCursor: string = null; - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - let graphQLResultWithPagination: ApolloQueryResult; - - return ( - await Promise.all([ - ...allRepos.map(async (r) => { - const issues: Issue[] = []; - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllIssuesOfAllReposOfAllOrgWithPagination, - variables: { - orgLogin: r.organization, - name: r.name, - cursorIssue: issueEndCursor, - date, - }, - }); - - issueEndCursor = - graphQLResultWithPagination.data.viewer.organization.repository - .issues.pageInfo.endCursor; - issues.push( - ...graphQLResultWithPagination.data.viewer.organization.repository.issues.edges.map( - (i) => { - const issue: Issue = new Issue(); - issue.id = i.node.id; - issue.repoId = r.id; - issue.state = i.node.state; - issue.createdAt = i.node.createdAt; - issue.closedAt = i.node.closedAt; - - return issue; - }, - ), - ); - } while ( - graphQLResultWithPagination.data.viewer.organization.repository - .issues.pageInfo.hasNextPage - ); - return issues; - }), - ]) - ).flatMap((i) => i); - } - - async getIssuesOfAllRepoOfUserWithPagination(date: Date): Promise { - let repoEndCursor: string = null; - let issueEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - - const issues: Issue[] = []; - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllIssuesOfAllRepoOfUserWithPagination, - variables: { - cursorRepo: repoEndCursor, - cursorIssue: issueEndCursor, - date, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; - - issues.push( - ...graphQLResultWithPagination.data.viewer.repositories.edges - .filter((r: Record) => !r.node.isInOrganization) - .map((r) => { - const issuesList: Issue[] = []; - do { - issueEndCursor = r.node.issues.pageInfo.endCursor; - issuesList.push( - ...r.node.issues.edges.map((i) => { - const issue: Issue = new Issue(); - issue.id = i.node.id; - issue.repoId = r.node.id; - issue.state = i.node.state; - issue.createdAt = i.node.createdAt; - issue.closedAt = i.node.closedAt; - - return issue; - }), - ); - } while (r.node.issues.pageInfo.hasNextPage); - return issuesList; - }) - .flatMap((i) => i), - ); - } while ( - graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage - ); - return issues; - } - - // Get all pull requests - async getPullRequestsOfAllRepoOfAllOrgWithPagination( - date: Date, - ): Promise { - let pullRequestEndCursor: string = null; - const allRepos = await this.getAllRepoOfAllOrgWithPagination(); - let graphQLResultWithPagination: ApolloQueryResult; - - return ( - await Promise.all([ - ...allRepos.map(async (r) => { - const pullRequests: PullRequest[] = []; - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllPullRequestsOfAllReposOfAllOrgWithPagination, - variables: { - orgLogin: r.organization, - name: r.name, - cursorPullRequest: pullRequestEndCursor, - }, - }); - - pullRequestEndCursor = - graphQLResultWithPagination.data.viewer.organization.repository - .pullRequests.pageInfo.endCursor; - pullRequests.push( - ...graphQLResultWithPagination.data.viewer.organization.repository.pullRequests.edges.map( - (p) => { - const pullRequest: PullRequest = new PullRequest(); - pullRequest.id = p.node.id; - pullRequest.repoId = r.id; - pullRequest.state = p.node.state; - pullRequest.createdAt = p.node.createdAt; - pullRequest.closedAt = p.node.closedAt; - - return pullRequest; - }, - ), - ); - } while ( - graphQLResultWithPagination.data.viewer.organization.repository - .pullRequests.pageInfo.hasNextPage - ); - return pullRequests; - }), - ]) - ).flatMap((pr) => pr); - } - - async getPullRequestsOfAllRepoOfUserWithPagination( - date: Date, - ): Promise { - let repoEndCursor: string = null; - let pullRequestEndCursor: string = null; - let graphQLResultWithPagination: ApolloQueryResult; - const pullRequests: PullRequest[] = []; - - do { - graphQLResultWithPagination = await this.apolloService - .githubClient() - .query({ - query: GetAllPullRequestsOfAllRepoOfUserWithPagination, - variables: { - cursorRepo: repoEndCursor, - cursorPullRequest: pullRequestEndCursor, - }, - }); - - repoEndCursor = - graphQLResultWithPagination.data.viewer.repositories.pageInfo.endCursor; - - pullRequests.push( - ...graphQLResultWithPagination.data.viewer.repositories.edges - .filter((r: Record) => !r.node.isInOrganization) - .map((r) => { - const pullRequestsList: PullRequest[] = []; - - do { - pullRequestEndCursor = r.node.pullRequests.pageInfo.endCursor; - pullRequestsList.push( - ...r.node.pullRequests.edges.map((p) => { - const pullRequest: PullRequest = new PullRequest(); - pullRequest.id = p.node.id; - pullRequest.repoId = r.node.id; - pullRequest.state = p.node.state; - pullRequest.createdAt = p.node.createdAt; - pullRequest.closedAt = p.node.closedAt; - - return pullRequest; - }), - ); - } while (r.node.pullRequests.pageInfo.hasNextPage); - return pullRequestsList; - }) - .flatMap((pr) => pr), - ); - } while ( - graphQLResultWithPagination.data.viewer.repositories.pageInfo.hasNextPage - ); - return pullRequests; - } -} diff --git a/api/src/modules/repo/gitlab/repo.gitlab.module.ts b/api/src/modules/repo/gitlab/repo.gitlab.module.ts deleted file mode 100644 index 5445a4c..0000000 --- a/api/src/modules/repo/gitlab/repo.gitlab.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RepoGitlabService } from './repo.gitlab.service'; - -@Module({ - imports: [], - exports: [RepoGitlabService], - providers: [RepoGitlabService], - controllers: [], -}) -export class RepoGitlabModule {} diff --git a/api/src/modules/repo/orgs.controller.ts b/api/src/modules/repo/orgs.controller.ts index 312a694..5a7a117 100644 --- a/api/src/modules/repo/orgs.controller.ts +++ b/api/src/modules/repo/orgs.controller.ts @@ -2,23 +2,23 @@ import { Controller, Get, Param } from '@nestjs/common'; import { User } from 'src/entities/user.entity'; import { GitProviderService } from '../git-provider/gitprovider.service'; import { USER } from '../users/users.decorator'; -import { RepoService } from './repo.service'; +import { OrgsService } from './orgs.service'; @Controller('/api/orgs') export class OrgsController { constructor( - private readonly repoService: RepoService, + private readonly orgsService: OrgsService, private readonly gitProviderService: GitProviderService, ) {} @Get('') async getOrgs(): Promise<{ login: string; isUser: boolean }[]> { - const orgs = await this.gitProviderService.getAllOrganizations(); + const orgs = await this.gitProviderService.getAllOrgs(); const profile = await this.gitProviderService.getProfile(); return [ { ...profile, isUser: true }, - ...orgs.map((o) => ({ login: o, isUser: false })), + ...orgs.map((o) => ({ login: o.login, isUser: false })), ]; } @@ -27,9 +27,9 @@ export class OrgsController { @USER() user: User, @Param('org') org: string, ): Promise { - return await this.repoService.getRepositories( - user.id, + return await this.orgsService.getRepositories( org === 'user' ? null : org, + user.id, ); } } diff --git a/api/src/modules/repo/orgs.service.ts b/api/src/modules/repo/orgs.service.ts new file mode 100644 index 0000000..5f1de95 --- /dev/null +++ b/api/src/modules/repo/orgs.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repo } from 'src/entities/repo.entity'; +import { Equal, In, IsNull, Not, Repository } from 'typeorm'; + +@Injectable() +export class OrgsService { + protected isPublic: boolean; + + constructor( + @InjectRepository(Repo) + protected repoRepository: Repository, + ) {} + + async getAllOrganizations(userId?: string): Promise { + let qb = this.repoRepository.createQueryBuilder('repo'); + if (!this.isPublic) { + qb = qb.innerJoin('repo.users', 'repoUsers', 'repoUsers.id = (:userId)', { + userId, + }); + } + + return ( + await qb + .where({ organization: Not(IsNull()) }) + .select('organization') + .distinct() + .getRawMany() + ).map((r: { organization: string }) => r.organization); + } + + async getRepositories( + organization: string | null, + userId?: string, + ): Promise { + let qb = this.repoRepository.createQueryBuilder('repo'); + if (!this.isPublic) { + qb = qb.innerJoin('repo.users', 'repoUsers', 'repoUsers.id = (:userId)', { + userId, + }); + } + return ( + await qb + .select('repo.name') + .where({ + organization: organization === null ? IsNull() : Equal(organization), + }) + .distinct() + .getRawMany() + ).map((r) => r.repo_name); + } + + async findByOrgByReposAndTime( + organization: string | null, + names: string[], + time: string, + userId?: string, + ): Promise { + const date = new Date(); + switch (time) { + case 'last day': + date.setHours(date.getHours() - 24); + break; + + case 'last week': + date.setHours(date.getHours() - 168); + break; + + case 'last month': + date.setMonth(date.getMonth() - 1); + break; + + case 'last 3 months': + date.setMonth(date.getMonth() - 3); + break; + + case 'last 6 months': + date.setMonth(date.getMonth() - 6); + break; + } + let qb = this.repoRepository.createQueryBuilder('repo'); + if (!this.isPublic) { + qb = qb.innerJoin('repo.users', 'repoUsers', 'repoUsers.id = (:userId)', { + userId, + }); + } + + return await qb + .innerJoinAndSelect('repo.commits', 'commits', 'commits.date >= :date', { + date, + }) + .leftJoinAndSelect('repo.issues', 'issues', 'issues.createdAt >= :date', { + date, + }) + .leftJoinAndSelect( + 'repo.pullRequests', + 'pullRequests', + 'pullRequests.createdAt >= :date', + { + date, + }, + ) + .where({ + name: In(Object.values(names || {})), + organization: organization === null ? IsNull() : organization, + }) + .getMany(); + } +} diff --git a/api/src/modules/repo/orgstats.controller.ts b/api/src/modules/repo/orgstats.controller.ts index 8e664b3..4c1a076 100644 --- a/api/src/modules/repo/orgstats.controller.ts +++ b/api/src/modules/repo/orgstats.controller.ts @@ -2,11 +2,11 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { User } from '@octokit/graphql-schema'; import { Repo } from 'src/entities/repo.entity'; import { USER } from '../users/users.decorator'; -import { RepoService } from './repo.service'; +import { OrgsService } from './orgs.service'; @Controller('/api/orgstats') export class OrgstatsController { - constructor(private readonly repoService: RepoService) {} + constructor(private readonly orgsService: OrgsService) {} @Get(':org') async getRepoStat( @@ -14,11 +14,11 @@ export class OrgstatsController { @Param('org') org: string, @Query('filters') { repositories, time }, ): Promise { - return await this.repoService.findByOrgByReposAndTime( - user.id, + return await this.orgsService.findByOrgByReposAndTime( org === 'user' ? null : org, repositories, time, + user.id, ); } } diff --git a/api/src/modules/repo/repo.module.ts b/api/src/modules/repo/repo.module.ts index 1ed6b6c..d838d1b 100644 --- a/api/src/modules/repo/repo.module.ts +++ b/api/src/modules/repo/repo.module.ts @@ -9,21 +9,17 @@ import { OrgsController } from './orgs.controller'; import { OrgstatsController } from './orgstats.controller'; import { GitProviderModule } from '../git-provider/gitprovider.module'; import { RepoService } from './repo.service'; -import { RepoGithubModule } from './github/repo.github.module'; -import { RepoGitlabModule } from './gitlab/repo.gitlab.module'; -import { RepoGithubService } from './github/repo.github.service'; -import { RepoGitlabService } from './gitlab/repo.gitlab.service'; +import { OrgsService } from './orgs.service'; +import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ TypeOrmModule.forFeature([Repo, Commit, Issue, PullRequest, User]), GitProviderModule, - RepoGithubModule, - RepoGitlabModule, - RepoModule, + HttpModule, ], exports: [RepoService], - providers: [RepoService, RepoGithubService, RepoGitlabService], + providers: [OrgsService, RepoService], controllers: [OrgsController, OrgstatsController], }) export class RepoModule {} diff --git a/api/src/modules/repo/repo.service.ts b/api/src/modules/repo/repo.service.ts index d032a4e..a1f1069 100644 --- a/api/src/modules/repo/repo.service.ts +++ b/api/src/modules/repo/repo.service.ts @@ -4,39 +4,13 @@ import { Commit } from 'src/entities/commit.entity'; import { Issue } from 'src/entities/issue.entity'; import { PullRequest } from 'src/entities/pullrequest.entity'; import { Repo } from 'src/entities/repo.entity'; -import { Equal, In, IsNull, Not, Repository } from 'typeorm'; -import { RepoGithubService } from './github/repo.github.service'; -import { RepoGitlabService } from './gitlab/repo.gitlab.service'; - -export interface Organization { - id: string; - login: string; - databaseId: number; -} - -export interface IRepoGitProvider { - auth(token: string): void; - getAllOrgWithPagination(): Promise; - getAllRepoOfAllOrgWithPagination(): Promise; - getAllRepoOfUserWithPagination(): Promise; - getCommitsOfAllRepoOfAllOrgWithPagination(date: Date): Promise; - getCommitsOfAllRepoOfUserWithPagination(date: Date): Promise; - getIssuesOfAllRepoOfAllOrgWithPagination(date: Date): Promise; - getIssuesOfAllRepoOfUserWithPagination(date: Date): Promise; - getPullRequestsOfAllRepoOfAllOrgWithPagination( - date: Date, - ): Promise; - getPullRequestsOfAllRepoOfUserWithPagination( - date: Date, - ): Promise; -} +import { Repository } from 'typeorm'; +import { GitProviderService } from '../git-provider/gitprovider.service'; @Injectable() export class RepoService { - #repoGitProvider: IRepoGitProvider; constructor( - @InjectRepository(Repo) - private repoRepository: Repository, + private readonly gitProviderService: GitProviderService, @InjectRepository(Commit) private commitRepository: Repository, @InjectRepository(Issue) @@ -45,175 +19,29 @@ export class RepoService { private pullRequestRepository: Repository, ) {} - auth(providerName: string, token: string): void { - if (providerName === 'github') { - this.#repoGitProvider = new RepoGithubService(); - } else if (providerName === 'gitlab') { - this.#repoGitProvider = new RepoGitlabService(); - } - this.#repoGitProvider.auth(token); - } - - async getAllOrganizations(userId: string): Promise { - return ( - await this.repoRepository - .createQueryBuilder('repo') - .innerJoin('repo.users', 'repoUsers', 'repoUsers.id = (:userId)', { - userId, - }) - .where({ organization: Not(IsNull()) }) - .select('organization') - .distinct() - .getRawMany() - ).map((r: { organization: string }) => r.organization); - } - - async getRepositories( - userId: string, - organization: string | null, - ): Promise { - return ( - await this.repoRepository - .createQueryBuilder('repo') - .innerJoin('repo.users', 'repoUsers', 'repoUsers.id = (:userId)', { - userId, - }) - .select('repo.name') - .where({ - organization: organization === null ? IsNull() : Equal(organization), - }) - .distinct() - .getRawMany() - ).map((r) => r.repo_name); - } - - findByOrgByReposAndTime( - userId: string, - organization: string | null, - names: string[], - time: string, - ): Promise { - const date = new Date(); - switch (time) { - case 'last day': - date.setHours(date.getHours() - 24); - break; - - case 'last week': - date.setHours(date.getHours() - 168); - break; - - case 'last month': - date.setMonth(date.getMonth() - 1); - break; - - case 'last 3 months': - date.setMonth(date.getMonth() - 3); - break; - - case 'last 6 months': - date.setMonth(date.getMonth() - 6); - break; - } - - return this.repoRepository - .createQueryBuilder('repo') - .innerJoinAndSelect('repo.commits', 'commits', 'commits.date >= :date', { - date, - }) - .leftJoinAndSelect('repo.issues', 'issues', 'issues.createdAt >= :date', { - date, - }) - .leftJoinAndSelect( - 'repo.pullRequests', - 'pullRequests', - 'pullRequests.createdAt >= :date', - { - date, - }, - ) - .innerJoinAndSelect('repo.users', 'users', 'users.id = :userId', { - userId, - }) - .where({ - name: In(Object.values(names || {})), - organization: organization === null ? IsNull() : organization, - }) - .getMany(); - } - - async upsert(id: string, repo: Repo): Promise { - await this.repoRepository.upsert( - { - id, - ...repo, - }, - ['id'], - ); - } - - // Get all organisations - async getAllOrgWithPagination(): Promise { - return await this.#repoGitProvider.getAllOrgWithPagination(); - } - - // Get all repositories - async getAllRepoOfAllOrgWithPagination(): Promise { - return await this.#repoGitProvider.getAllRepoOfAllOrgWithPagination(); - } - - async getAllRepoOfUserWithPagination(): Promise { - return await this.#repoGitProvider.getAllRepoOfUserWithPagination(); - } - // Get all commits - async getCommitsOfAllRepoOfAllOrgWithPagination(date: Date): Promise { - const commits = - await this.#repoGitProvider.getCommitsOfAllRepoOfAllOrgWithPagination( - date, - ); - await this.commitRepository.save(commits); - } - - async getCommitsOfAllRepoOfUserWithPagination(date: Date): Promise { - const commits = - await this.#repoGitProvider.getCommitsOfAllRepoOfUserWithPagination(date); - this.commitRepository.save(commits); + async getCommitsOfRepos(repos: Repo[], date: Date): Promise { + const commits = await this.gitProviderService.getCommitsOfRepos( + repos, + date, + ); + await this.commitRepository.save(commits, { chunk: 100 }); } // Get all issues - async getIssuesOfAllRepoOfAllOrgWithPagination(date: Date): Promise { - const issues = - await this.#repoGitProvider.getIssuesOfAllRepoOfAllOrgWithPagination( - date, - ); - this.issueRepository.save(issues); - } - - async getIssuesOfAllRepoOfUserWithPagination(date: Date): Promise { - const issues = - await this.#repoGitProvider.getIssuesOfAllRepoOfUserWithPagination(date); - this.issueRepository.save(issues); + async getIssuesOfRepos(repos: Repo[], date: Date): Promise { + const issues = await this.gitProviderService.getIssuesOfRepos(repos, date); + this.issueRepository.save(issues, { chunk: 100 }); } // Get all pull requests - async getPullRequestsOfAllRepoOfAllOrgWithPagination( - date: Date, - ): Promise { - const pullRequests = - await this.#repoGitProvider.getPullRequestsOfAllRepoOfAllOrgWithPagination( - date, - ); - this.pullRequestRepository.save(pullRequests); - } - - async getPullRequestsOfAllRepoOfUserWithPagination( - date: Date, - ): Promise { - const pullRequests = - await this.#repoGitProvider.getPullRequestsOfAllRepoOfUserWithPagination( - date, - ); - this.pullRequestRepository.save(pullRequests); + async getPullRequestsOfRepos(repos: Repo[], date: Date): Promise { + const pullRequests = await this.gitProviderService.getPullRequestsOfRepos( + repos, + date, + ); + this.pullRequestRepository.save(pullRequests, { + chunk: 100, + }); } } diff --git a/api/src/modules/synchronize/admin.controller.ts b/api/src/modules/synchronize/admin.controller.ts index 4b72eb1..d6ce630 100644 --- a/api/src/modules/synchronize/admin.controller.ts +++ b/api/src/modules/synchronize/admin.controller.ts @@ -6,6 +6,7 @@ import * as Bull from 'bull'; import express from 'express'; import { CRON_SYNCHRONIZATION_QUEUE, + PUBLIC_SYNCHRONIZATION_QUEUE, USER_SYNCHRONIZATION_QUEUE, } from './synchronize.constants'; @@ -21,7 +22,11 @@ export class QueueAdminController { ) { const serverAdapter = new ExpressAdapter(); serverAdapter.setBasePath(rootPath); - const queues = [USER_SYNCHRONIZATION_QUEUE, CRON_SYNCHRONIZATION_QUEUE].map( + const queues = [ + USER_SYNCHRONIZATION_QUEUE, + CRON_SYNCHRONIZATION_QUEUE, + PUBLIC_SYNCHRONIZATION_QUEUE, + ].map( (queue) => new Bull(queue, { redis: { @@ -33,7 +38,7 @@ export class QueueAdminController { ); const router = serverAdapter.getRouter() as express.Express; createBullBoard({ - queues: [new BullAdapter(queues[0]), new BullAdapter(queues[1])], + queues: [...queues.map((q) => new BullAdapter(q))], serverAdapter, }); req.url = req.url.replace(rootPath, '/'); diff --git a/api/src/modules/synchronize/consumer.service.ts b/api/src/modules/synchronize/consumer.service.ts index ecf5ed3..9a42b1d 100644 --- a/api/src/modules/synchronize/consumer.service.ts +++ b/api/src/modules/synchronize/consumer.service.ts @@ -1,5 +1,6 @@ import { Processor, Process } from '@nestjs/bull'; import { Job } from 'bull'; +import { NotificationsService } from '../notifications/notifications.service'; import { UsersService } from '../users/users.service'; import { UserSynchronizeJob } from './producer.service'; import { USER_SYNCHRONIZATION_QUEUE } from './synchronize.constants'; @@ -10,22 +11,45 @@ export class ConsumerService { constructor( private readonly usersService: UsersService, private readonly synchronizeService: SynchronizeService, + private readonly notificationsService: NotificationsService, ) {} @Process() async transcode(job: Job): Promise { const user = await this.usersService.findOne(job.data.userId); - this.synchronizeService.auth(user.gitProviderName, user.gitProviderToken); - + this.notificationsService.setCurrentUser(user); + this.synchronizeService.auth( + job.data.gitProviderName, + job.data.gitProviderToken, + ); const now = new Date(); const synchronizationDate = new Date(job.data.fromDate); - await job.progress(5); - await this.synchronizeService.synchronize(synchronizationDate, job); + try { + await job.progress(5); + await this.synchronizeService.synchronize(synchronizationDate, job); + + await job.progress(95); + await this.usersService.update(user.id, { + lastSynchronize: now, + }); - await job.progress(95); - await this.usersService.update(user.id, { - lastSynchronize: now, - }); + await this.notificationsService.notify({ + event: 'synchronize_job_ended', + data: { + jobId: job.id, + }, + }); + } catch (e) { + console.log(e); + await this.notificationsService.notify({ + event: 'synchronize_job_error', + data: { + jobId: job.id, + errorMessage: e.message, + errorStack: e.stack, + }, + }); + } } } diff --git a/api/src/modules/synchronize/producer.service.ts b/api/src/modules/synchronize/producer.service.ts index ee7fce8..ebdae36 100644 --- a/api/src/modules/synchronize/producer.service.ts +++ b/api/src/modules/synchronize/producer.service.ts @@ -2,24 +2,31 @@ import { Injectable } from '@nestjs/common'; import Bull, { Job, Queue } from 'bull'; import { InjectQueue } from '@nestjs/bull'; import { USER_SYNCHRONIZATION_QUEUE } from './synchronize.constants'; - -export interface UserSynchronizeJob { +import { Organization } from 'src/common/types'; +import { Repo } from 'src/entities/repo.entity'; +export interface SynchronizeJob { fromDate: string; + orgs: Organization[]; + repos: Repo[]; + gitProviderName: string; + gitProviderToken?: string; +} +export interface UserSynchronizeJob extends SynchronizeJob { userId: string; } @Injectable() -export class ProducerService { +export class ProducerService { constructor( @InjectQueue(USER_SYNCHRONIZATION_QUEUE) - private userSynchronizationQueue: Queue, + protected readonly synchronizationQueue: Queue, ) {} async getJob(jobId: Bull.JobId) { - return await this.userSynchronizationQueue.getJob(jobId); + return await this.synchronizationQueue.getJob(jobId); } - async addJob(job: UserSynchronizeJob): Promise> { + async addJob(job: T): Promise> { const now = new Date(); const defaultFromDate = new Date(now.getTime()); defaultFromDate.setMonth(now.getMonth() - 6); @@ -28,18 +35,18 @@ export class ProducerService { fromDate: defaultFromDate.toISOString(), }; - const actives = await this.userSynchronizationQueue.getActive(); - const waitings = await this.userSynchronizationQueue.getWaiting(); + const actives = await this.synchronizationQueue.getActive(); + const waitings = await this.synchronizationQueue.getWaiting(); - if ( - [...actives, ...waitings].some( - (j) => j.data.fromDate === job.fromDate && j.data.userId === job.userId, - ) - ) { + const existingJob = [...actives, ...waitings].find((j) => + Object.entries(j).every(([k, v]) => v === job[k]), + ); + if (existingJob) { console.log('has active, cancelling'); + return existingJob; } - const consolidatedJob: UserSynchronizeJob = { ...defaults, ...job }; - return await this.userSynchronizationQueue.add(consolidatedJob); + const consolidatedJob: T = { ...defaults, ...job }; + return await this.synchronizationQueue.add(consolidatedJob); } } diff --git a/api/src/modules/synchronize/synchronize.constants.ts b/api/src/modules/synchronize/synchronize.constants.ts index 7fe0f4c..8f27dd7 100644 --- a/api/src/modules/synchronize/synchronize.constants.ts +++ b/api/src/modules/synchronize/synchronize.constants.ts @@ -1,3 +1,5 @@ export const USER_SYNCHRONIZATION_QUEUE = 'sync-organization'; export const CRON_SYNCHRONIZATION_QUEUE = 'sync-cron'; + +export const PUBLIC_SYNCHRONIZATION_QUEUE = 'sync-organization-public'; diff --git a/api/src/modules/synchronize/synchronize.controller.ts b/api/src/modules/synchronize/synchronize.controller.ts index 73b3e25..31ebc8b 100644 --- a/api/src/modules/synchronize/synchronize.controller.ts +++ b/api/src/modules/synchronize/synchronize.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Post } from '@nestjs/common'; import { Job } from 'bull'; import { User } from 'src/entities/user.entity'; +import { GitProviderService } from '../git-provider/gitprovider.service'; +import { NotificationsService } from '../notifications/notifications.service'; import { ProducerService, UserSynchronizeJob, @@ -9,17 +11,21 @@ import { USER } from '../users/users.decorator'; @Controller('/api/synchronize') export class SynchronizeController { - constructor(private readonly synchronizeProducerService: ProducerService) {} + constructor( + private readonly synchronizeProducerService: ProducerService, + private readonly notificationsService: NotificationsService, + private readonly gitProviderService: GitProviderService, + ) {} @Get('jobs/:id') - async getAllRepoStat( + async getSynchronizationJob( @Param('id') id: string, ): Promise> { return await this.synchronizeProducerService.getJob(id); } @Post('jobs') - async getAllRepoStatOfAllOrg( + async createSynchronizationJob( @USER() user: User, ): Promise> { let date: Date; @@ -30,9 +36,22 @@ export class SynchronizeController { date = user.lastSynchronize; } + const orgs = await this.gitProviderService.getAllOrgs(); const job = await this.synchronizeProducerService.addJob({ userId: user.id, + orgs, + repos: user.repos, fromDate: date.toISOString(), + gitProviderName: user.gitProviderName, + gitProviderToken: user.gitProviderToken, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + await this.notificationsService.notify({ + event: 'synchronize_job_created', + data: { + jobId: job.id, + }, }); return job; diff --git a/api/src/modules/synchronize/synchronize.module.ts b/api/src/modules/synchronize/synchronize.module.ts index 5fcb4e6..455ddcb 100644 --- a/api/src/modules/synchronize/synchronize.module.ts +++ b/api/src/modules/synchronize/synchronize.module.ts @@ -1,7 +1,9 @@ +import { HttpModule } from '@nestjs/axios'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from 'src/entities/user.entity'; +import { GitProviderModule } from '../git-provider/gitprovider.module'; import { RepoModule } from '../repo/repo.module'; import { UsersModule } from '../users/users.module'; import { UsersService } from '../users/users.service'; @@ -30,6 +32,8 @@ import { SynchronizeService } from './synchronize.service'; ), UsersModule, RepoModule, + GitProviderModule, + HttpModule, ], controllers: [SynchronizeController, QueueAdminController], exports: [ProducerService], diff --git a/api/src/modules/synchronize/synchronize.service.ts b/api/src/modules/synchronize/synchronize.service.ts index f0fac4c..f288ee4 100644 --- a/api/src/modules/synchronize/synchronize.service.ts +++ b/api/src/modules/synchronize/synchronize.service.ts @@ -1,32 +1,31 @@ import { Injectable } from '@nestjs/common'; import { Job } from 'bull'; +import { GitProviderService } from '../git-provider/gitprovider.service'; import { RepoService } from '../repo/repo.service'; +import { SynchronizeJob } from './producer.service'; @Injectable() export class SynchronizeService { - constructor(private readonly repoService: RepoService) {} + constructor( + protected readonly gitProviderService: GitProviderService, + protected readonly repoService: RepoService, + ) {} auth(gitProviderName: string, token: string): void { - this.repoService.auth(gitProviderName, token); + this.gitProviderService.auth(gitProviderName, token); } - async synchronize(date: Date, job?: Job) { + async synchronize(date: Date, job?: Job) { // Get Commits - await this.repoService.getCommitsOfAllRepoOfAllOrgWithPagination(date); - await job?.progress(30); - await this.repoService.getCommitsOfAllRepoOfUserWithPagination(date); + await this.repoService.getCommitsOfRepos(job.data.repos, date); await job?.progress(40); // Get Issues - await this.repoService.getIssuesOfAllRepoOfAllOrgWithPagination(date); - await job?.progress(60); - await this.repoService.getIssuesOfAllRepoOfUserWithPagination(date); + await this.repoService.getIssuesOfRepos(job.data.repos, date); await job?.progress(70); // Get Pull Requests - await this.repoService.getPullRequestsOfAllRepoOfAllOrgWithPagination(date); - await job?.progress(80); - await this.repoService.getPullRequestsOfAllRepoOfUserWithPagination(date); + await this.repoService.getPullRequestsOfRepos(job.data.repos, date); await job?.progress(90); } } diff --git a/api/src/modules/users/users.controller.ts b/api/src/modules/users/users.controller.ts index a8257d8..9d6ca9b 100644 --- a/api/src/modules/users/users.controller.ts +++ b/api/src/modules/users/users.controller.ts @@ -1,11 +1,8 @@ -import { Body, Controller, Delete, Get, Put, Req } from '@nestjs/common'; -import { Request } from 'express'; +import { Controller, Get, Put } from '@nestjs/common'; import { Repo } from 'src/entities/repo.entity'; import { User } from 'src/entities/user.entity'; import { GitProviderService } from '../git-provider/gitprovider.service'; -import { RepoService } from '../repo/repo.service'; import { USER } from './users.decorator'; -import { UserDTO, UserProfileDTO } from './users.dto'; import { UsersService } from './users.service'; @Controller('/api/users') @@ -13,7 +10,6 @@ export class UsersController { constructor( private readonly usersService: UsersService, private readonly gitProviderService: GitProviderService, - private readonly repoService: RepoService, ) {} @Get('me') @@ -21,44 +17,13 @@ export class UsersController { return await this.usersService.findOne(user.id); } - @Put('me') - async setUser( - @Req() request: Request, - @Body() userDto: UserDTO, - ): Promise<{ status: string }> { - await this.usersService.upsert(request['token'].sub, userDto); - return { status: 'ok' }; - } - - @Put('me/profile') - async setUserProfile( - @Req() request: Request, - @Body() userProfileDto: UserProfileDTO, - ): Promise<{ status: string }> { - await this.usersService.updateProfile(request['token'].sub, userProfileDto); - return { status: 'ok' }; - } - @Put('me/repositories') async setRepositories(@USER() user: User): Promise<{ status: string }> { - const reposOrg = await this.repoService.getAllRepoOfAllOrgWithPagination(); - const reposUser = await this.repoService.getAllRepoOfUserWithPagination(); + const orgs = await this.gitProviderService.getAllOrgs(); + const reposOrg = await this.gitProviderService.getAllRepoOfOrgs(orgs); + const reposUser = await this.gitProviderService.getAllReposOfUser(); const reposEntities: Repo[] = reposOrg.concat(reposUser); await this.usersService.addRepositories(user, reposEntities); return { status: 'ok' }; } - - @Delete('me/gitProvider') - async deleteGithubAccess( - @USER() user: User, - @Req() request: Request, - ): Promise<{ status: string }> { - await this.gitProviderService.revokeAccess(user.gitProviderToken); - await this.usersService.update(request['token'].sub, { - gitProviderId: null, - gitProviderToken: null, - gitProviderName: null, - }); - return { status: 'ok' }; - } } diff --git a/api/src/modules/users/users.dto.ts b/api/src/modules/users/users.dto.ts index ec184e9..0cb2fb9 100644 --- a/api/src/modules/users/users.dto.ts +++ b/api/src/modules/users/users.dto.ts @@ -2,7 +2,6 @@ export class UserDTO { email?: string; name?: string; avatarUrl?: string; - gitProviderId?: string; gitProviderToken?: string; gitProviderName?: string; lastSynchronize?: Date; diff --git a/api/src/queries/org/list-commit-with-pagination.graphql b/api/src/queries/org/list-commit-with-pagination.graphql index 969a017..f761e72 100644 --- a/api/src/queries/org/list-commit-with-pagination.graphql +++ b/api/src/queries/org/list-commit-with-pagination.graphql @@ -1,35 +1,28 @@ -query GetAllCommitsOfAllReposOfAllOrgWithPagination( - $orgLogin: String! +query GetAllCommitsOfRepository( $name: String! + $owner: String! $date: GitTimestamp ) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - defaultBranchRef { - target { - ... on Commit { - history(since: $date) { - edges { - node { - ... on Commit { - author { - date - email - name - } - id - committedDate - changedFilesIfAvailable - additions - deletions - } + repository(name: $name, owner: $owner) { + id + name + defaultBranchRef { + target { + ... on Commit { + history(since: $date) { + edges { + node { + ... on Commit { + author { + date + email + name } + id + committedDate + changedFilesIfAvailable + additions + deletions } } } diff --git a/api/src/queries/org/list-issue-with-pagination.graphql b/api/src/queries/org/list-issue-with-pagination.graphql index d43ecc2..4760da1 100644 --- a/api/src/queries/org/list-issue-with-pagination.graphql +++ b/api/src/queries/org/list-issue-with-pagination.graphql @@ -1,31 +1,24 @@ -query GetAllIssuesOfAllReposOfAllOrgWithPagination( - $orgLogin: String! +query GetAllIssuesOfRepoWithPagination( + $owner: String! $name: String! $cursorIssue: String $date: DateTime ) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - issues(first: 100, after: $cursorIssue, filterBy: { since: $date }) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - state - closedAt - createdAt - } - } + repository(name: $name, owner: $owner) { + id + name + issues(first: 100, after: $cursorIssue, filterBy: { since: $date }) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + state + closedAt + createdAt } } } diff --git a/api/src/queries/org/list-org-with-pagination.graphql b/api/src/queries/org/list-org-with-pagination.graphql index 5f2fddb..9749933 100644 --- a/api/src/queries/org/list-org-with-pagination.graphql +++ b/api/src/queries/org/list-org-with-pagination.graphql @@ -11,7 +11,6 @@ query GetAllOrgsWithPagination($cursorOrgs: String) { node { id login - databaseId } } } diff --git a/api/src/queries/org/list-pull-requests-with-pagination.graphql b/api/src/queries/org/list-pull-requests-with-pagination.graphql index 40e731e..46318db 100644 --- a/api/src/queries/org/list-pull-requests-with-pagination.graphql +++ b/api/src/queries/org/list-pull-requests-with-pagination.graphql @@ -1,30 +1,23 @@ -query GetAllPullRequestsOfAllReposOfAllOrgWithPagination( - $orgLogin: String! +query GetAllPullRequestsOfRepoWithPagination( + $owner: String! $name: String! $cursorPullRequest: String ) { - viewer { - login - organization(login: $orgLogin) { - id - login - repository(name: $name) { - id - name - pullRequests(first: 100, after: $cursorPullRequest) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - state - closedAt - createdAt - } - } + repository(name: $name, owner: $owner) { + id + name + pullRequests(first: 100, after: $cursorPullRequest) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + state + closedAt + createdAt } } } diff --git a/api/src/queries/org/list-repo-with-pagination.graphql b/api/src/queries/org/list-repo-with-pagination.graphql index 6a57c6d..e34e5eb 100644 --- a/api/src/queries/org/list-repo-with-pagination.graphql +++ b/api/src/queries/org/list-repo-with-pagination.graphql @@ -1,18 +1,24 @@ -query GetAllReposOfOrgWithPagination($orgLogin: String!, $cursorRepo: String) { - viewer { - login - organization(login: $orgLogin) { - repositories(first: 100, after: $cursorRepo) { - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - id - name - } +query GetAllReposOfOrgWithPagination( + $orgLogin: String! + $privacy: RepositoryPrivacy + $cursorRepo: String +) { + organization(login: $orgLogin) { + repositories( + first: 100 + after: $cursorRepo + privacy: $privacy + orderBy: { field: STARGAZERS, direction: DESC } + ) { + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + id + name } } } diff --git a/api/src/queries/schema.graphql b/api/src/queries/schema.graphql index 37c0981..878579e 100644 --- a/api/src/queries/schema.graphql +++ b/api/src/queries/schema.graphql @@ -16,6 +16,5 @@ interface Node { type Organization implements Node { id: ID! - databaseId: Int login: String! } diff --git a/api/src/queries/user/list-repo-with-pagination.graphql b/api/src/queries/user/list-repo-with-pagination.graphql index c7f84e8..5820af2 100644 --- a/api/src/queries/user/list-repo-with-pagination.graphql +++ b/api/src/queries/user/list-repo-with-pagination.graphql @@ -12,6 +12,10 @@ query GetAllReposOfUserWithPagination($cursorRepo: String) { id name isInOrganization + owner { + id + login + } } } } diff --git a/client/.env b/client/.env index 42a1da6..d83bb2a 100644 --- a/client/.env +++ b/client/.env @@ -1,7 +1,2 @@ -GITHUB_ID=GITHUB_ID -GITHUB_SECRET=GITHUB_SECRET -GITLAB_ID=GITLAB_ID -GITLAB_SECRET=GITLAB_SECRET -NEXTAUTH_SECRET=SECRET SERVER_API_URL=http://localhost:3001 NEXT_PUBLIC_API_URL=http://localhost:3001 \ No newline at end of file diff --git a/client/components/dashboard/Issues/Table.tsx b/client/components/dashboard/Issues/Table.tsx deleted file mode 100644 index 5e83bb4..0000000 --- a/client/components/dashboard/Issues/Table.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Issue, RepositoryStatistics } from "../../../common/types"; - -const IssuesTable = ({ - issues, - repositories, -}: { - issues: Issue[]; - repositories: RepositoryStatistics[]; -}) => { - return ( -
- - - - - - - - - - - - - {issues?.map((item) => { - const repo = repositories.find((r) => r.id === item.repoId); - return ( - - - - - - - - ); - })} - -
- Issues -
- ID - - Repository - - Created at - - Closed at - - State -
- {item.id} - {repo?.name} - {new Date(item.createdAt).toISOString()} - - {item.closedAt ? new Date(item.closedAt).toISOString() : ""} - {item.state}
-
- ); -}; - -export default IssuesTable; diff --git a/client/components/dashboard/PullRequests/Table.tsx b/client/components/dashboard/PullRequests/Table.tsx deleted file mode 100644 index 8917ca4..0000000 --- a/client/components/dashboard/PullRequests/Table.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PullRequest, RepositoryStatistics } from "../../../common/types"; - -const PullRequestsTable = ({ - pullRequests, - repositories, -}: { - pullRequests: PullRequest[]; - repositories: RepositoryStatistics[]; -}) => { - return ( -
- - - - - - - - - - - - - {pullRequests?.map((item) => { - const repo = repositories.find((r) => r.id === item.repoId); - return ( - - - - - - - - ); - })} - -
- Oull requests -
- ID - - Repository - - Created at - - Closed at - - State -
- {item.id} - {repo?.name} - {new Date(item.createdAt).toISOString()} - - {item.closedAt ? new Date(item.closedAt).toISOString() : ""} - {item.state}
-
- ); -}; - -export default PullRequestsTable; diff --git a/client/components/dashboard/Synchronize.tsx b/client/components/dashboard/Synchronize.tsx deleted file mode 100644 index 263dacc..0000000 --- a/client/components/dashboard/Synchronize.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useAtom } from "jotai"; -import { getInstance } from "../../services/api"; -import { asyncRefreshUser, userState } from "../../services/state"; -import Button from "../common/Button"; - -function Synchronize() { - const [isLoadingSynchronize, setIsLoadingSynchronize] = useState(false); - const [isSynchronizedDisabled, setIsSynchronizedDisabled] = useState(false); - const [runningJob, setRunningJob] = useState<{ finishedOn: number } | null>( - null - ); - const [user] = useAtom(userState); - const [, refreshUser] = useAtom(asyncRefreshUser); - const hasSynchronized = useRef(false); - - const onClickSynchronize = useCallback(() => { - const pollSynchronize = (jobId: string | number) => { - const timer = self.setInterval(() => { - getInstance() - .get(`/api/synchronize/jobs/${jobId}`) - .then((res) => { - setRunningJob(res.data); - if (res.data.finishedOn) { - self.clearInterval(timer); - refreshUser(); - } - }); - }, 2000); - }; - - setIsLoadingSynchronize(true); - setIsSynchronizedDisabled(true); - getInstance() - .post("/api/synchronize/jobs") - .then((res) => { - setIsLoadingSynchronize(false); - setIsSynchronizedDisabled(false); - setRunningJob(res.data); - pollSynchronize(res.data.id); - }); - }, [refreshUser]); - - useEffect(() => { - if (user !== null && !user.lastSynchronize && !hasSynchronized.current) { - hasSynchronized.current = true; - onClickSynchronize(); - } - }, [onClickSynchronize, user]); - - useEffect(() => { - refreshUser(); - }, [refreshUser]); - - return ( -
- - {user?.lastSynchronize && ( -
- Synchronized at {user?.lastSynchronize} -
- )} -
- ); -} - -export default Synchronize; diff --git a/client/components/index/Banner.tsx b/client/components/index/Banner.tsx deleted file mode 100644 index 005fe9e..0000000 --- a/client/components/index/Banner.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from "next/link"; -import { SITE_INFOS } from "../../common/constants"; - -export const Banner = () => { - return ( -
-
-
-
-

- {SITE_INFOS.subtitle} -

-

- {SITE_INFOS.description} -

-
- -
-
-
- Gitvision -
-
- ); -}; - -export default Banner; diff --git a/client/components/index/Features.tsx b/client/components/index/Features.tsx deleted file mode 100644 index 0ede710..0000000 --- a/client/components/index/Features.tsx +++ /dev/null @@ -1,134 +0,0 @@ -const features = [ - { - title: "Organization insights", - icon: ( - - - - ), - description: ( - <> - Select your organization or main repositories and display KPIs and - graphs about it - - ), - list: ["Contributors", "Commit activity", "Issues", "Pull requests"], - }, - { - title: "Filter by what's important to you", - icon: ( - - - - ), - description: ( - <> - Access KPIs history and filter the graphs and indicators by multiple - axes - - ), - list: ["Organizations", "Repositories", "Date range", "More to come"], - }, - { - title: "Get alerts and realtime notifications", - icon: ( - - - - ), - description: ( - <> - Get weekly summarized activity notifications as well as realtime - notifications on your prefered platform (coming soon) - - ), - list: ["Activity report", "Contributors ranking", "Issues reports"], - }, -]; -export const Features = () => { - return ( -
-
-
-

- Explore and use new level of codebase information -

-
-
-

- As a tech leader or manager, get insights to gamify and improve - efficiency of your team organization & processes -

-
-
-
- {features.map((f) => ( -
-
- {f.icon} -
-
{f.title}
-

{f.description}

-
    - {f.list.map((l) => ( -
  • - - - - - - {l} -
  • - ))} -
-
- ))} -
-
- ); -}; - -export default Features; diff --git a/client/components/index/Pricings.tsx b/client/components/index/Pricings.tsx deleted file mode 100644 index 5d582fe..0000000 --- a/client/components/index/Pricings.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import Link from "next/link"; - -const pricings = [ - { - title: "Free", - tag: "In beta", - price: "$0", - features: [ - "Unlimited organization", - "Unlimited repositories", - "6 month data retention", - ], - link: ( - - - Get started - - - ), - }, -]; - -const Pricings = () => { - return ( -
-
-
-

- Our Pricing -

-
-

- Transparent pricing. Pay as you grow. -

-

- All features are free. Pay for more retention and usage. -

-
-
- {pricings.map((p) => ( -
-
-
- {p.tag} -
-
-
-
{p.title}
-
-
{p.price}
-
/ mo
-
-
- {p.features.map((f) => ( -
- {f} -
- ))} -
-
-
{p.link}
-
- ))} -
-
- ); -}; - -export default Pricings; diff --git a/client/components/layout/Footer.tsx b/client/components/layout/Footer.tsx index 63c253d..d1b7614 100644 --- a/client/components/layout/Footer.tsx +++ b/client/components/layout/Footer.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { SITE_INFOS } from "../../common/constants"; +import { SITE_INFOS } from "../../core/common/constants"; const FooterLink = ({ label, href }: { label: string; href: string }) => ( diff --git a/client/components/layout/Navbar.tsx b/client/components/layout/Navbar.tsx deleted file mode 100644 index 344fd9f..0000000 --- a/client/components/layout/Navbar.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { Menu, Transition } from "@headlessui/react"; -import { signOut, useSession } from "next-auth/react"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { ForwardedRef, forwardRef, Fragment, useState } from "react"; -import { SITE_INFOS } from "../../common/constants"; -import { classNames } from "../../common/utils"; -import Button from "../common/Button"; - -// This component is to forward onClick event from Menu.Item to close the menu on item click -const CustomLink = forwardRef( - ( - props: { href: string; children: React.ReactNode; className: string }, - ref: ForwardedRef - ) => { - const { href, children, className, ...rest } = props; - return ( - - - {children} - - - ); - } -); - -CustomLink.displayName = "CustomLink"; - -export const Navbar = () => { - const { data: session, status } = useSession(); - const router = useRouter(); - - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); - - const mainMenuItems = - session && router.pathname != "/" - ? [ - { - label: "Dashboard", - link: "/dashboard", - }, - ] - : [ - { - label: "Features", - link: "/#features", - }, - { - label: "Pricing", - link: "/#pricing", - }, - ]; - - const userMenuItems = [ - { - label: "Dashboard", - link: "/dashboard", - }, - { - label: "Profile", - link: "/profile", - }, - { - label: "Settings", - link: "/settings", - }, - ]; - - return ( -
-
-
- - - {SITE_INFOS.title} - - - {status !== "loading" && ( - - )} -
- {!session && ( - - )} - {session && ( - - )} -
- - {isMenuOpen && ( -
-
-
- -
- -
-
- -
-
- )} -
-
-
- ); -}; diff --git a/client/common/constants.ts b/client/core/common/constants.ts similarity index 81% rename from client/common/constants.ts rename to client/core/common/constants.ts index 6e67fd5..375258a 100644 --- a/client/common/constants.ts +++ b/client/core/common/constants.ts @@ -1,5 +1,3 @@ -export const SESSION_COOKIE_NAME = "next-auth.session-token"; - export const SITE_INFOS = { title: "Gitvision", subtitle: "Get actionable insights for your Git organization", diff --git a/client/common/types.ts b/client/core/common/types.ts similarity index 97% rename from client/common/types.ts rename to client/core/common/types.ts index 1adb650..2b0c5a7 100644 --- a/client/common/types.ts +++ b/client/core/common/types.ts @@ -1,6 +1,5 @@ export interface User { id: string; - gitProviderId: string; gitProviderName: string; email: string; name: string; diff --git a/client/common/utils.ts b/client/core/common/utils.ts similarity index 100% rename from client/common/utils.ts rename to client/core/common/utils.ts diff --git a/client/components/common/Alert.tsx b/client/core/components/common/Alert.tsx similarity index 100% rename from client/components/common/Alert.tsx rename to client/core/components/common/Alert.tsx diff --git a/client/components/common/Button.tsx b/client/core/components/common/Button.tsx similarity index 100% rename from client/components/common/Button.tsx rename to client/core/components/common/Button.tsx diff --git a/client/components/common/Charts/Bump.tsx b/client/core/components/common/Charts/Bump.tsx similarity index 100% rename from client/components/common/Charts/Bump.tsx rename to client/core/components/common/Charts/Bump.tsx diff --git a/client/components/common/Charts/Line.tsx b/client/core/components/common/Charts/Line.tsx similarity index 100% rename from client/components/common/Charts/Line.tsx rename to client/core/components/common/Charts/Line.tsx diff --git a/client/components/common/Dropdown.tsx b/client/core/components/common/Dropdown.tsx similarity index 100% rename from client/components/common/Dropdown.tsx rename to client/core/components/common/Dropdown.tsx diff --git a/client/components/common/Input.tsx b/client/core/components/common/Input.tsx similarity index 100% rename from client/components/common/Input.tsx rename to client/core/components/common/Input.tsx diff --git a/client/components/common/Loader.tsx b/client/core/components/common/Loader.tsx similarity index 100% rename from client/components/common/Loader.tsx rename to client/core/components/common/Loader.tsx diff --git a/client/components/common/Progress.tsx b/client/core/components/common/Progress.tsx similarity index 100% rename from client/components/common/Progress.tsx rename to client/core/components/common/Progress.tsx diff --git a/client/components/common/Tabs.tsx b/client/core/components/common/Tabs.tsx similarity index 100% rename from client/components/common/Tabs.tsx rename to client/core/components/common/Tabs.tsx diff --git a/client/components/common/Variant.tsx b/client/core/components/common/Variant.tsx similarity index 100% rename from client/components/common/Variant.tsx rename to client/core/components/common/Variant.tsx diff --git a/client/components/dashboard/ActiveRepositories/History.tsx b/client/core/components/dashboard/ActiveRepositories/History.tsx similarity index 100% rename from client/components/dashboard/ActiveRepositories/History.tsx rename to client/core/components/dashboard/ActiveRepositories/History.tsx diff --git a/client/components/dashboard/ActiveRepositories/Index.tsx b/client/core/components/dashboard/ActiveRepositories/Index.tsx similarity index 100% rename from client/components/dashboard/ActiveRepositories/Index.tsx rename to client/core/components/dashboard/ActiveRepositories/Index.tsx diff --git a/client/components/dashboard/ActiveRepositories/Table.tsx b/client/core/components/dashboard/ActiveRepositories/Table.tsx similarity index 100% rename from client/components/dashboard/ActiveRepositories/Table.tsx rename to client/core/components/dashboard/ActiveRepositories/Table.tsx diff --git a/client/components/dashboard/Contributors/History.tsx b/client/core/components/dashboard/Contributors/History.tsx similarity index 100% rename from client/components/dashboard/Contributors/History.tsx rename to client/core/components/dashboard/Contributors/History.tsx diff --git a/client/components/dashboard/Contributors/Index.tsx b/client/core/components/dashboard/Contributors/Index.tsx similarity index 100% rename from client/components/dashboard/Contributors/Index.tsx rename to client/core/components/dashboard/Contributors/Index.tsx diff --git a/client/components/dashboard/Contributors/RankingHistory.tsx b/client/core/components/dashboard/Contributors/RankingHistory.tsx similarity index 100% rename from client/components/dashboard/Contributors/RankingHistory.tsx rename to client/core/components/dashboard/Contributors/RankingHistory.tsx diff --git a/client/components/dashboard/Contributors/Table.tsx b/client/core/components/dashboard/Contributors/Table.tsx similarity index 100% rename from client/components/dashboard/Contributors/Table.tsx rename to client/core/components/dashboard/Contributors/Table.tsx diff --git a/client/pages/dashboard.tsx b/client/core/components/dashboard/Dashboard.tsx similarity index 74% rename from client/pages/dashboard.tsx rename to client/core/components/dashboard/Dashboard.tsx index 06a41b9..8d22cd2 100644 --- a/client/pages/dashboard.tsx +++ b/client/core/components/dashboard/Dashboard.tsx @@ -1,25 +1,37 @@ -import { useAtom } from "jotai"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Contributor, Issue, KpiCategory, PullRequest, RepositoryStatistics, -} from "../common/types"; -import Loader from "../components/common/Loader"; -import Progress from "../components/common/Progress"; -import ActiveRepositories from "../components/dashboard/ActiveRepositories/Index"; -import Contributors from "../components/dashboard/Contributors/Index"; -import DashboardFilters from "../components/dashboard/Filters"; -import useSynchronize from "../components/dashboard/hooks/useSynchronize"; -import Issues from "../components/dashboard/Issues/Index"; -import Kpis from "../components/dashboard/Kpis"; -import PullRequests from "../components/dashboard/PullRequests/Index"; -import { getInstance } from "../services/api"; -import { asyncRefreshUser } from "../services/state"; - -function Dashboard() { +} from "../../common/types"; +import DashboardFilters from "./Filters"; +import Issues from "./Issues/Index"; +import Kpis from "./Kpis"; +import PullRequests from "./PullRequests/Index"; +import { getInstance } from "../../services/api"; +import Contributors from "./Contributors/Index"; +import ActiveRepositories from "./ActiveRepositories/Index"; +import Loader from "../common/Loader"; +import Progress from "../common/Progress"; +import useSynchronize from "./hooks/useSynchronize"; + +export interface DashboardComponentProps { + initialSynchronization: boolean; + initialOrganization?: string; + baseOrgStatsUrl: string; + baseOrgSearchUrl: string; + onFiltersApplied?: (filters: Record) => void; +} + +function DashboardComponent({ + initialSynchronization, + initialOrganization, + baseOrgStatsUrl, + baseOrgSearchUrl, + onFiltersApplied, +}: DashboardComponentProps) { const [contributors, setContributors] = useState([]); const [repositories, setRepositories] = useState([]); const [issues, setIssues] = useState([]); @@ -28,11 +40,14 @@ function Dashboard() { const [selectedKpi, setSelectedKpi] = useState( KpiCategory.Contributors ); - const [, refreshUser] = useAtom(asyncRefreshUser); - const { isSynchronizing, runningJob } = useSynchronize(); + + const { isSynchronizing, runningJob } = useSynchronize({ + initialSynchronization, + }); const onApplyFilters = (filters: Record) => { - if (filters.repositories) { + onFiltersApplied?.(filters); + if (filters.repositories && filters.time) { setFilters(filters); changeDashboard(filters); } @@ -77,7 +92,7 @@ function Dashboard() { const changeDashboard = async (filters: Record) => { const resp = await getInstance().get( - `/api/orgstats/${encodeURIComponent(filters.organization)}`, + `${baseOrgStatsUrl}/${encodeURIComponent(filters.organization)}`, { params: { filters, @@ -97,16 +112,12 @@ function Dashboard() { ); }; - useEffect(() => { - refreshUser(); - }, [refreshUser]); - return ( <> { - onApplyFilters(filters); - }} + onChange={onApplyFilters} + initialOrganization={initialOrganization} + baseOrgSearchUrl={baseOrgSearchUrl} /> {isSynchronizing && ( @@ -167,4 +178,9 @@ function Dashboard() { ); } -export default Dashboard; +DashboardComponent.defaultProps = { + baseOrgStatsUrl: "/api/orgstats", + baseOrgSearchUrl: "/api/orgs", +}; + +export default DashboardComponent; diff --git a/client/components/dashboard/Filters.tsx b/client/core/components/dashboard/Filters.tsx similarity index 82% rename from client/components/dashboard/Filters.tsx rename to client/core/components/dashboard/Filters.tsx index b856236..3a8d9d8 100644 --- a/client/components/dashboard/Filters.tsx +++ b/client/core/components/dashboard/Filters.tsx @@ -18,11 +18,17 @@ export interface Filters { branches?: string[]; } +interface DashboardFiltersProps { + onChange: (filters: Filters) => void; + initialOrganization?: string; + baseOrgSearchUrl: string; +} + function DashboardFilters({ onChange, -}: { - onChange: (filters: Filters) => void; -}) { + initialOrganization, + baseOrgSearchUrl, +}: DashboardFiltersProps) { const [organizations, setOrganizations] = useState< { login: string; @@ -46,12 +52,20 @@ function DashboardFilters({ }, [JSON.stringify(filters)]); useEffect(() => { + const newFilters = { + ...filters, + }; + let hasChanged = false; if (!filters.organization && organizations.length) { - setFilters({ - ...filters, - organization: organizations[0].login, - time: times[2].label, - }); + newFilters.organization = organizations[0].login; + hasChanged = true; + } + if (!filters.time) { + newFilters.time = times[2].label; + hasChanged = true; + } + if (hasChanged) { + setFilters(newFilters); } }, [filters, organizations]); @@ -61,7 +75,7 @@ function DashboardFilters({ const org = organizations.find((o) => o.login === filters.organization); getInstance() .get( - `/api/orgs/${ + `${baseOrgSearchUrl}/${ org?.isUser ? "user" : encodeURIComponent(filters.organization) }/repos` ) @@ -76,14 +90,16 @@ function DashboardFilters({ } }, [filters.organization, organizations]); - const loadOrganizations = () => + useEffect(() => { + if (initialOrganization) { + setOrganizations([{ login: initialOrganization, isUser: false }]); + setFilters({ organization: initialOrganization }); + return; + } getInstance() - .get("/api/orgs") + .get(baseOrgSearchUrl) .then((res) => setOrganizations(res.data)); - - useEffect(() => { - loadOrganizations(); - }, []); + }, [initialOrganization]); return ( <> diff --git a/client/components/dashboard/Issues/History.tsx b/client/core/components/dashboard/Issues/History.tsx similarity index 100% rename from client/components/dashboard/Issues/History.tsx rename to client/core/components/dashboard/Issues/History.tsx diff --git a/client/components/dashboard/Issues/Index.tsx b/client/core/components/dashboard/Issues/Index.tsx similarity index 96% rename from client/components/dashboard/Issues/Index.tsx rename to client/core/components/dashboard/Issues/Index.tsx index a03e98f..16c964c 100644 --- a/client/components/dashboard/Issues/Index.tsx +++ b/client/core/components/dashboard/Issues/Index.tsx @@ -14,7 +14,7 @@ const Issues = ({ }) => { const tabs = [ { - label: "Current", + label: "Count", component: , }, { diff --git a/client/core/components/dashboard/Issues/Table.tsx b/client/core/components/dashboard/Issues/Table.tsx new file mode 100644 index 0000000..a10b059 --- /dev/null +++ b/client/core/components/dashboard/Issues/Table.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState } from "react"; +import { Issue, RepositoryStatistics } from "../../../common/types"; + +interface GroupedIssue { + state: string; + count: number; + lastCreatedAtRepo: string; + lastCreatedAtDate: Date; + lastClosedAtRepo: string; + lastClosedAtDate: string; +} + +const IssuesTable = ({ + issues, + repositories, +}: { + issues: Issue[]; + repositories: RepositoryStatistics[]; +}) => { + const [groupedIssues, setGroupedIssues] = useState([]); + + const groupByIssuesState = () => { + const groupedIssues: GroupedIssue[] = []; + issues.reduce((res: Record, issue: Issue) => { + if (!res[issue.state]) { + res[issue.state] = { + state: issue.state, + count: 0, + lastClosedAtDate: issue.closedAt, + lastClosedAtRepo: issue.repoId, + lastCreatedAtDate: issue.createdAt, + lastCreatedAtRepo: issue.repoId, + } as GroupedIssue; + groupedIssues.push(res[issue.state]); + } + res[issue.state].count += 1; + if ( + issue.closedAt && + Date.parse(res[issue.state].lastClosedAtDate) < + Date.parse(issue.closedAt) + ) { + res[issue.state].lastClosedAtDate = issue.closedAt; + res[issue.state].lastClosedAtRepo = issue.repoId; + } + if (res[issue.state].lastCreatedAtDate < issue.createdAt) { + res[issue.state].lastCreatedAtDate = issue.createdAt; + res[issue.state].lastCreatedAtRepo = issue.repoId; + } + return res; + }, {} as Record); + return groupedIssues; + }; + + useEffect(() => { + setGroupedIssues(groupByIssuesState()); + }, []); + + return ( +
+ + + + + + + + + + + + + + {groupedIssues?.map((item) => { + const lastClosedrepo = repositories.find( + (r) => r.id === item.lastClosedAtRepo + ); + const lastCreatedrepo = repositories.find( + (r) => r.id === item.lastCreatedAtRepo + ); + return ( + + + + + + + + + ); + })} + +
+ Issues +
+ State + + Count + + Last created at date + + Last created at repository + + Last closed at date + + Last closed at repository +
+ {item.state.toLocaleLowerCase()} + + {item.count} + + {new Date(item.lastCreatedAtDate).toISOString()} + {lastCreatedrepo?.name} + {item.lastClosedAtDate + ? new Date(item.lastClosedAtDate).toISOString() + : ""} + + {item.lastClosedAtDate ? lastClosedrepo?.name : ""} +
+
+ ); +}; + +export default IssuesTable; diff --git a/client/components/dashboard/Kpis.tsx b/client/core/components/dashboard/Kpis.tsx similarity index 100% rename from client/components/dashboard/Kpis.tsx rename to client/core/components/dashboard/Kpis.tsx diff --git a/client/components/dashboard/PullRequests/History.tsx b/client/core/components/dashboard/PullRequests/History.tsx similarity index 100% rename from client/components/dashboard/PullRequests/History.tsx rename to client/core/components/dashboard/PullRequests/History.tsx diff --git a/client/components/dashboard/PullRequests/Index.tsx b/client/core/components/dashboard/PullRequests/Index.tsx similarity index 96% rename from client/components/dashboard/PullRequests/Index.tsx rename to client/core/components/dashboard/PullRequests/Index.tsx index d3d5bed..08192ad 100644 --- a/client/components/dashboard/PullRequests/Index.tsx +++ b/client/core/components/dashboard/PullRequests/Index.tsx @@ -14,7 +14,7 @@ const PullRequests = ({ }) => { const tabs = [ { - label: "Current", + label: "Count", component: ( { + const [groupedPullRequests, setGroupedPullRequests] = useState< + GroupedPullRequest[] + >([]); + + const groupByPullrequestsState = () => { + const groupedPullRequests: GroupedPullRequest[] = []; + pullRequests.reduce( + (res: Record, pullRequest: PullRequest) => { + if (!res[pullRequest.state]) { + res[pullRequest.state] = { + state: pullRequest.state, + count: 0, + lastClosedAtDate: pullRequest.closedAt, + lastClosedAtRepo: pullRequest.repoId, + lastCreatedAtDate: pullRequest.createdAt, + lastCreatedAtRepo: pullRequest.repoId, + } as GroupedPullRequest; + groupedPullRequests.push(res[pullRequest.state]); + } + res[pullRequest.state].count += 1; + if ( + pullRequest.closedAt && + Date.parse(res[pullRequest.state].lastClosedAtDate) < + Date.parse(pullRequest.closedAt) + ) { + res[pullRequest.state].lastClosedAtDate = pullRequest.closedAt; + res[pullRequest.state].lastClosedAtRepo = pullRequest.repoId; + } + if (res[pullRequest.state].lastCreatedAtDate < pullRequest.createdAt) { + res[pullRequest.state].lastCreatedAtDate = pullRequest.createdAt; + res[pullRequest.state].lastCreatedAtRepo = pullRequest.repoId; + } + return res; + }, + {} as Record + ); + return groupedPullRequests; + }; + + useEffect(() => { + setGroupedPullRequests(groupByPullrequestsState()); + }, []); + return ( +
+ + + + + + + + + + + + + + {groupedPullRequests?.map((item) => { + const lastClosedrepo = repositories.find( + (r) => r.id === item.lastClosedAtRepo + ); + const lastCreatedrepo = repositories.find( + (r) => r.id === item.lastCreatedAtRepo + ); + return ( + + + + + + + + + ); + })} + +
+ Pull requests +
+ State + + Count + + Last created at date + + Last created at repository + + Last closed at date + + Last closed at repository +
+ {item.state.toLocaleLowerCase()} + + {item.count} + + {new Date(item.lastCreatedAtDate).toISOString()} + {lastCreatedrepo?.name} + {item.lastClosedAtDate + ? new Date(item.lastClosedAtDate).toISOString() + : ""} + + {item.lastClosedAtDate ? lastClosedrepo?.name : ""} +
+
+ ); +}; + +export default PullRequestsTable; diff --git a/client/components/dashboard/hooks/useSynchronize.ts b/client/core/components/dashboard/hooks/useSynchronize.ts similarity index 77% rename from client/components/dashboard/hooks/useSynchronize.ts rename to client/core/components/dashboard/hooks/useSynchronize.ts index e724892..df32557 100644 --- a/client/components/dashboard/hooks/useSynchronize.ts +++ b/client/core/components/dashboard/hooks/useSynchronize.ts @@ -3,7 +3,11 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { getInstance } from "../../../services/api"; import { asyncRefreshUser, userState } from "../../../services/state"; -const useSynchronize = () => { +const useSynchronize = ({ + initialSynchronization, +}: { + initialSynchronization: boolean; +}) => { const [isSynchronizing, setIsSynchronizing] = useState(false); const [runningJob, setRunningJob] = useState<{ finishedOn: number; @@ -39,11 +43,22 @@ const useSynchronize = () => { }, [refreshUser]); useEffect(() => { - if (user !== null && !user.lastSynchronize && !hasSynchronized.current) { + if ( + initialSynchronization && + user !== null && + !user.lastSynchronize && + !hasSynchronized.current + ) { hasSynchronized.current = true; synchronize(); } - }, [synchronize, user]); + }, [initialSynchronization, synchronize, user]); + + useEffect(() => { + if (initialSynchronization) { + refreshUser(); + } + }, [initialSynchronization, refreshUser]); return { isSynchronizing, diff --git a/client/services/api.ts b/client/core/services/api.ts similarity index 100% rename from client/services/api.ts rename to client/core/services/api.ts diff --git a/client/services/state.ts b/client/core/services/state.ts similarity index 100% rename from client/services/state.ts rename to client/core/services/state.ts diff --git a/client/package-lock.json b/client/package-lock.json index 4d9debe..561c826 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,7 +17,6 @@ "jotai": "^1.9.0", "jsonwebtoken": "^8.5.1", "next": "12.3.1", - "next-auth": "^4.12.3", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.4.0" @@ -41,6 +40,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.4" @@ -588,14 +588,6 @@ "node": ">= 8" } }, - "node_modules/@panva/hkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz", - "integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/@react-spring/animated": { "version": "9.4.5", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.4.5.tgz", @@ -1354,14 +1346,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/core-js-pure": { "version": "3.25.5", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.5.tgz", @@ -2863,14 +2847,6 @@ "dev": true, "license": "ISC" }, - "node_modules/jose": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.0.tgz", - "integrity": "sha512-KEhB/eLGLomWGPTb+/RNbYsTjIyx03JmbqAyIyiXBuNSa7CmNrJd5ysFhblayzs/e/vbOPMUaLnjHUMhGp4yLw==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/jotai": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.9.0.tgz", @@ -3149,6 +3125,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -3303,36 +3280,6 @@ } } }, - "node_modules/next-auth": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.12.3.tgz", - "integrity": "sha512-kfJjYOH2or/y+pEBkeA6O2MxIXOKXNiOKLKhrQHsaRqMDttEQX0yEK3xXVTB1yB7xbMLGcMvsIS3d23BC/K8/A==", - "dependencies": { - "@babel/runtime": "^7.16.3", - "@panva/hkdf": "^1.0.1", - "cookie": "^0.5.0", - "jose": "^4.9.3", - "oauth": "^0.9.15", - "openid-client": "^5.1.0", - "preact": "^10.6.3", - "preact-render-to-string": "^5.1.19", - "uuid": "^8.3.2" - }, - "engines": { - "node": "^12.19.0 || ^14.15.0 || ^16.13.0" - }, - "peerDependencies": { - "next": "^12.2.5", - "nodemailer": "^6.6.5", - "react": "^17.0.2 || ^18", - "react-dom": "^17.0.2 || ^18" - }, - "peerDependenciesMeta": { - "nodemailer": { - "optional": true - } - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -3379,11 +3326,6 @@ "node": ">=0.10.0" } }, - "node_modules/oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3505,14 +3447,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oidc-token-hash": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", - "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3523,28 +3457,6 @@ "wrappy": "1" } }, - "node_modules/openid-client": { - "version": "5.1.10", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.1.10.tgz", - "integrity": "sha512-KYAtkxTuUwTvjAmH0QMFFP3i9l0+XhP2/blct6Q9kn+DUJ/lu8/g/bI8ghSgxz9dJLm/9cpB/1uLVGTcGGY0hw==", - "dependencies": { - "jose": "^4.1.4", - "lru-cache": "^6.0.0", - "object-hash": "^2.0.1", - "oidc-token-hash": "^5.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/openid-client/node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3800,26 +3712,6 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, - "node_modules/preact": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.1.tgz", - "integrity": "sha512-1Wz5PCRm6Fg+6BTXWJHhX4wRK9MZbZBHuwBqfZlOdVm2NqPe8/rjYpufvYCwJSGb9layyzB2jTTXfpCTynLqFQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.5.tgz", - "integrity": "sha512-rEBn42C3Wh+AjPxXUbDkb6xw0cTJQgxdYlp6ytUR1uBZF647Wn6ykkopMeQlRl7ggX+qnYYjZ4Hs1abZENl7ww==", - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3830,11 +3722,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3959,6 +3846,7 @@ "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true, "license": "MIT" }, "node_modules/regexp.prototype.flags": { @@ -4515,14 +4403,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4585,6 +4465,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -4614,6 +4495,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -4940,11 +4822,6 @@ "fastq": "^1.6.0" } }, - "@panva/hkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz", - "integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==" - }, "@react-spring/animated": { "version": "9.4.5", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.4.5.tgz", @@ -5451,11 +5328,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, "core-js-pure": { "version": "3.25.5", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.5.tgz", @@ -6516,11 +6388,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "jose": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.0.tgz", - "integrity": "sha512-KEhB/eLGLomWGPTb+/RNbYsTjIyx03JmbqAyIyiXBuNSa7CmNrJd5ysFhblayzs/e/vbOPMUaLnjHUMhGp4yLw==" - }, "jotai": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.9.0.tgz", @@ -6718,6 +6585,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -6822,22 +6690,6 @@ } } }, - "next-auth": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.12.3.tgz", - "integrity": "sha512-kfJjYOH2or/y+pEBkeA6O2MxIXOKXNiOKLKhrQHsaRqMDttEQX0yEK3xXVTB1yB7xbMLGcMvsIS3d23BC/K8/A==", - "requires": { - "@babel/runtime": "^7.16.3", - "@panva/hkdf": "^1.0.1", - "cookie": "^0.5.0", - "jose": "^4.9.3", - "oauth": "^0.9.15", - "openid-client": "^5.1.0", - "preact": "^10.6.3", - "preact-render-to-string": "^5.1.19", - "uuid": "^8.3.2" - } - }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -6855,11 +6707,6 @@ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, - "oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6937,11 +6784,6 @@ "es-abstract": "^1.19.1" } }, - "oidc-token-hash": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", - "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6951,24 +6793,6 @@ "wrappy": "1" } }, - "openid-client": { - "version": "5.1.10", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.1.10.tgz", - "integrity": "sha512-KYAtkxTuUwTvjAmH0QMFFP3i9l0+XhP2/blct6Q9kn+DUJ/lu8/g/bI8ghSgxz9dJLm/9cpB/1uLVGTcGGY0hw==", - "requires": { - "jose": "^4.1.4", - "lru-cache": "^6.0.0", - "object-hash": "^2.0.1", - "oidc-token-hash": "^5.0.1" - }, - "dependencies": { - "object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" - } - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -7113,30 +6937,12 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, - "preact": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.1.tgz", - "integrity": "sha512-1Wz5PCRm6Fg+6BTXWJHhX4wRK9MZbZBHuwBqfZlOdVm2NqPe8/rjYpufvYCwJSGb9layyzB2jTTXfpCTynLqFQ==" - }, - "preact-render-to-string": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.5.tgz", - "integrity": "sha512-rEBn42C3Wh+AjPxXUbDkb6xw0cTJQgxdYlp6ytUR1uBZF647Wn6ykkopMeQlRl7ggX+qnYYjZ4Hs1abZENl7ww==", - "requires": { - "pretty-format": "^3.8.0" - } - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" - }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7220,7 +7026,8 @@ "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "regexp.prototype.flags": { "version": "1.4.3", @@ -7563,11 +7370,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7610,7 +7412,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yaml": { "version": "1.10.2", diff --git a/client/package.json b/client/package.json index 6ae5dca..e5bff94 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,6 @@ "jotai": "^1.9.0", "jsonwebtoken": "^8.5.1", "next": "12.3.1", - "next-auth": "^4.12.3", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.4.0" diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 36f96af..3ff0c07 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -1,49 +1,37 @@ import "../styles/globals.css"; import type { AppProps } from "next/app"; import Head from "next/head"; -import { SessionProvider, useSession } from "next-auth/react"; -import { Session } from "next-auth/core/types"; -import { Navbar } from "../components/layout/Navbar"; import Footer from "../components/layout/Footer"; import React, { useEffect } from "react"; import { useAtom } from "jotai"; -import { asyncRefreshUser } from "../services/state"; +import { asyncRefreshUser } from "../core/services/state"; const InnerComponent = ({ children }: { children: React.ReactNode }) => { - const session = useSession(); const [, refreshUser] = useAtom(asyncRefreshUser); useEffect(() => { - if (session.status === "authenticated") { - refreshUser(); - } - }, [refreshUser, session.status]); + refreshUser(); + }, [refreshUser]); return <>{children}; }; function MyApp({ Component, - pageProps: { session, ...pageProps }, -}: AppProps & { pageProps: { session: Session; disableLayout?: boolean } }) { + pageProps, +}: AppProps & { pageProps: { disableLayout?: boolean } }) { return ( - + <> Gitvision - {!pageProps.disableLayout && ( - <> - -
- - - -
-