diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b949063a4b..9901aad97c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12440,6 +12440,11 @@ "dev": true } } + }, + "zod": { + "version": "3.20.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.6.tgz", + "integrity": "sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==" } } } diff --git a/frontend/package.json b/frontend/package.json index 6ee1e634b4..8e5cf19276 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,7 +30,8 @@ "vue": "^3.2.47", "vue-loading-overlay": "^5.0.3", "vue-observe-visibility": "^1.0.0", - "vuex": "^4.0.2" + "vuex": "^4.0.2", + "zod": "^3.20.6" }, "devDependencies": { "@babel/eslint-parser": "^7.17.0", diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts new file mode 100644 index 0000000000..dc4417341a --- /dev/null +++ b/frontend/src/types/types.ts @@ -0,0 +1,47 @@ +import { FileResult } from './zod/authorship-type'; +import { + AuthorFileTypeContributions, + CommitResultRaw, + Commits, +} from './zod/commits-type'; +import { RepoRaw } from './zod/summary-type'; + +// We add these three fields in setContributionOfCommitResultsAndInsertRepoId of utils/api.ts +export interface CommitResult extends CommitResultRaw { + repoId: string; + insertions: number; + deletions: number; +} + +// Similar to AuthorDailyContributions, but uses the updated CommitResult with the three new fields +export interface DailyCommit { + commitResults: CommitResult[]; + date: string; +} + +// Similar to DailyCommit, but contains the total insertions and deletions for all CommitResults +export interface Commit extends DailyCommit { + deletions: number; + insertions: number; +} + +export interface User { + checkedFileTypeContribution: number; + commits?: Commit[]; + dailyCommits: DailyCommit[]; + displayName: string; + fileTypeContribution: AuthorFileTypeContributions; + location: string; + name: string; + repoId: string; + repoName: string; + searchPath: string; + variance: number; +} + +// We add these three fields in loadCommits and loadAuthorship of utils/api.ts +export interface Repo extends RepoRaw { + commits?: Commits; + files?: FileResult[]; + users?: User[]; +} diff --git a/frontend/src/types/window.ts b/frontend/src/types/window.ts new file mode 100644 index 0000000000..19946c4ae7 --- /dev/null +++ b/frontend/src/types/window.ts @@ -0,0 +1,72 @@ +import JSZip from 'jszip'; +import User from '../utils/user'; +import { Repo, User as UserType } from './types'; +import { AuthorshipSchema } from './zod/authorship-type'; +import { AuthorDailyContributions } from './zod/commits-type'; +import { DomainUrlMap, ErrorMessage } from './zod/summary-type'; + +// Declares the types for all the global variables under the window object +export {}; + +interface comparatorFunction { + (a: any, b: any): -1 | 0 | 1; +} + +interface sortingFunction { + (item: any, sortingOption?: string): any; +} + +interface api { + loadJSON: (fname: string) => Promise; + loadSummary: () => Promise<{ + creationDate: string, + reportGenerationTime: string, + errorMessages: { [key: string]: ErrorMessage }, + names: string[], + } | null>; + loadCommits: (repoName: string) => Promise; + loadAuthorship: (repoName: string) => Promise; + setContributionOfCommitResultsAndInsertRepoId: (dailyCommits: AuthorDailyContributions[], repoId: string) => void; +} + +declare global { + interface Window { + $: (id: string) => HTMLElement | null; + enquery: (key: string, val: string) => string; + REPOSENSE_REPO_URL: string; + HOME_PAGE_URL: string; + UNSUPPORTED_INDICATOR: string; + DAY_IN_MS: number; + HASH_DELIMITER: string; + REPOS: { [key: string]: Repo }; + hashParams: { [key: string]: string }; + isMacintosh: boolean; + REPORT_ZIP: JSZip | null; + deactivateAllOverlays: () => void; + getDateStr: (date: Date) => string; + getHexToRGB: (color: string) => number[]; + getFontColor: (color: string) => string; + addHash: (newKey: string, newVal: string) => void; + removeHash: (key: string) => void; + encodeHash: () => void; + decodeHash: () => void; + comparator: (fn: sortingFunction, sortingOption: string) => comparatorFunction; + filterUnsupported: (string: string) => string | undefined; + getAuthorLink: (repoId: string, author: string) => string | undefined; + getRepoLinkUnfiltered: (repoId: string) => string; + getRepoLink: (repoId: string) => string | undefined; + getBranchLink: (repoId: string, branch: string) => string | undefined; + getCommitLink: (repoId: string, commitHash: string) => string | undefined; + getBlameLink: (repoId: string, branch: string, filepath: string) => string | undefined; + getHistoryLink: (repoId: string, branch: string, filepath: string) => string | undefined; + getGroupName: (group: UserType[], filterGroupSelection: string) => string; + getAuthorDisplayName: (authorRepos: Repo[]) => string; + api: api; + sinceDate: string; + untilDate: string; + repoSenseVersion: string; + isSinceDateProvided: boolean; + isUntilDateProvided: boolean; + DOMAIN_URL_MAP: DomainUrlMap; + } +} diff --git a/frontend/src/types/zod/authorship-type.ts b/frontend/src/types/zod/authorship-type.ts new file mode 100644 index 0000000000..bf2dcd6cfb --- /dev/null +++ b/frontend/src/types/zod/authorship-type.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +const lineSchema = z.object({ + lineNumber: z.number(), + author: z.object({ gitId: z.string() }), + content: z.string(), +}); + +const fileResult = z.object({ + path: z.string(), + fileType: z.string(), + lines: z.array(lineSchema), + authorContributionMap: z.record(z.number()), +}); + +// Contains the zod validation schema for the authorship.json file + +export const authorshipSchema = z.array(fileResult); + +// Export typescript types +export type AuthorshipSchema = z.infer; +export type FileResult = z.infer; diff --git a/frontend/src/types/zod/commits-type.ts b/frontend/src/types/zod/commits-type.ts new file mode 100644 index 0000000000..0d174d72a2 --- /dev/null +++ b/frontend/src/types/zod/commits-type.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +const fileTypesAndContributionSchema = z.object({ + insertions: z.number(), + deletions: z.number(), +}); + +const commitResult = z.object({ + hash: z.string(), + messageTitle: z.string(), + messageBody: z.string(), + tags: z.array(z.string()).optional(), + fileTypesAndContributionMap: z.record(fileTypesAndContributionSchema), +}); + +const authorDailyContributionsSchema = z.object({ + date: z.string(), + commitResults: z.array(commitResult), +}); + +const authorFileTypeContributionsSchema = z.record(z.number()); + +// Contains the zod validation schema for the commits.json file + +export const commitsSchema = z.object({ + authorDailyContributionsMap: z.record(z.array(authorDailyContributionsSchema)), + authorFileTypeContributionMap: z.record(authorFileTypeContributionsSchema), + authorContributionVariance: z.record(z.number()), + authorDisplayNameMap: z.record(z.string()), +}); + +// Export typescript types +export type Commits = z.infer; +export type CommitResultRaw = z.infer; +export type AuthorDailyContributions = z.infer; +export type AuthorFileTypeContributions = z.infer; diff --git a/frontend/src/types/zod/summary-type.ts b/frontend/src/types/zod/summary-type.ts new file mode 100644 index 0000000000..a390bf4957 --- /dev/null +++ b/frontend/src/types/zod/summary-type.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; + +const locationSchema = z.object({ + location: z.string(), + repoName: z.string(), + organization: z.string(), + domainName: z.string(), +}); + +const repoSchema = z.object({ + location: locationSchema, + branch: z.string(), + displayName: z.string(), + outputFolderName: z.string(), +}); + +const errorSchema = z.object({ + repoName: z.string(), + errorMessage: z.string(), +}); + +const urlSchema = z.object({ + BASE_URL: z.string(), + BLAME_PATH: z.string(), + BRANCH: z.string(), + COMMIT_PATH: z.string(), + HISTORY_PATH: z.string(), + REPO_URL: z.string(), +}); + +const supportedDomainUrlMapSchema = z.record(urlSchema); + +// Contains the zod validation schema for the summary.json file + +export const summarySchema = z.object({ + repoSenseVersion: z.string(), + reportGeneratedTime: z.string(), + reportGenerationTime: z.string(), + zoneId: z.string(), + reportTitle: z.string(), + repos: z.array(repoSchema), + errorSet: z.array(errorSchema), + sinceDate: z.string(), + untilDate: z.string(), + isSinceDateProvided: z.boolean(), + isUntilDateProvided: z.boolean(), + supportedDomainUrlMap: supportedDomainUrlMapSchema, +}); + +// Export typescript types +export type DomainUrlMap = z.infer; +export type RepoRaw = z.infer; +export type ErrorMessage = z.infer; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.ts similarity index 83% rename from frontend/src/utils/api.js rename to frontend/src/utils/api.ts index b470f0cf7e..57271e0876 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.ts @@ -1,3 +1,7 @@ +import { DailyCommit, CommitResult } from '../types/types'; +import { authorshipSchema } from '../types/zod/authorship-type'; +import { commitsSchema } from '../types/zod/commits-type'; +import { ErrorMessage, summarySchema } from '../types/zod/summary-type'; import User from './user'; // utility functions // @@ -30,7 +34,7 @@ window.getDateStr = function getDateStr(date) { window.getHexToRGB = function getHexToRGB(color) { // to convert color from hex code to rgb format const arr = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); - return arr.slice(1).map((val) => parseInt(val, 16)); + return arr ? arr.slice(1).map((val) => parseInt(val, 16)) : []; }; window.getFontColor = function getFontColor(color) { @@ -64,7 +68,7 @@ window.encodeHash = function encodeHash() { }; window.decodeHash = function decodeHash() { - const hashParams = {}; + const hashParams: { [key: string]: string } = {}; const hashIndex = window.location.href.indexOf(HASH_ANCHOR); const parameterString = hashIndex === -1 ? '' : window.location.href.slice(hashIndex + 1); @@ -76,8 +80,8 @@ window.decodeHash = function decodeHash() { try { hashParams[key] = decodeURIComponent(val); } catch (error) { - this.userUpdated = false; - this.isLoading = false; + (this as any).userUpdated = false; + (this as any).isLoading = false; } } }); @@ -199,10 +203,11 @@ window.api = { }, async loadSummary() { window.REPOS = {}; - let data = {}; + let data; try { - data = await this.loadJSON('summary.json'); - } catch (error) { + const json = await this.loadJSON('summary.json'); + data = summarySchema.parse(json); + } catch (error: any) { if (error.message === 'Unable to read summary.json.') { return null; } @@ -216,14 +221,14 @@ window.api = { window.isUntilDateProvided = data.isUntilDateProvided; document.title = data.reportTitle || document.title; - const errorMessages = {}; + const errorMessages: { [key: string]: ErrorMessage } = {}; Object.entries(data.errorSet).forEach(([repoName, message]) => { errorMessages[repoName] = message; }); window.DOMAIN_URL_MAP = data.supportedDomainUrlMap; - const names = []; + const names: string[] = []; data.repos.forEach((repo) => { const repoName = `${repo.displayName}`; window.REPOS[repoName] = repo; @@ -239,33 +244,37 @@ window.api = { async loadCommits(repoName) { const folderName = window.REPOS[repoName].outputFolderName; - const commits = await this.loadJSON(`${folderName}/commits.json`); - const res = []; + const json = await this.loadJSON(`${folderName}/commits.json`); + const commits = commitsSchema.parse(json); + + const res: User[] = []; const repo = window.REPOS[repoName]; Object.keys(commits.authorDisplayNameMap).forEach((author) => { if (author) { - const obj = { - name: author, - repoId: repoName, - variance: commits.authorContributionVariance[author], - displayName: commits.authorDisplayNameMap[author], - dailyCommits: commits.authorDailyContributionsMap[author], - fileTypeContribution: commits.authorFileTypeContributionMap[author], - }; - - this.setContributionOfCommitResultsAndInsertRepoId(obj.dailyCommits, obj.repoId); + this.setContributionOfCommitResultsAndInsertRepoId(commits.authorDailyContributionsMap[author], repoName); const searchParams = [ repo.displayName, - obj.displayName, author, + commits.authorDisplayNameMap[author], + author, ]; - obj.searchPath = searchParams.join('_').toLowerCase(); - obj.repoName = `${repo.displayName}`; - obj.location = `${repo.location.location}`; + // commits and checkedFileTypeContribution are set in c-summary + const user = new User({ + name: author, + repoId: repoName, + variance: commits.authorContributionVariance[author], + displayName: commits.authorDisplayNameMap[author], + commits: [], + dailyCommits: commits.authorDailyContributionsMap[author] as DailyCommit[], + fileTypeContribution: commits.authorFileTypeContributionMap[author], + searchPath: searchParams.join('_').toLowerCase(), + repoName: `${repo.displayName}`, + location: `${repo.location.location}`, + checkedFileTypeContribution: 0, + }); - const user = new User(obj); res.push(user); } }); @@ -279,7 +288,8 @@ window.api = { loadAuthorship(repoName) { const folderName = window.REPOS[repoName].outputFolderName; return this.loadJSON(`${folderName}/authorship.json`) - .then((files) => { + .then((json) => { + const files = authorshipSchema.parse(json); window.REPOS[repoName].files = files; return files; }); @@ -290,10 +300,10 @@ window.api = { setContributionOfCommitResultsAndInsertRepoId(dailyCommits, repoId) { dailyCommits.forEach((commit) => { commit.commitResults.forEach((result) => { - result.repoId = repoId; - result.insertions = Object.values(result.fileTypesAndContributionMap) + (result as CommitResult).repoId = repoId; + (result as CommitResult).insertions = Object.values(result.fileTypesAndContributionMap) .reduce((acc, fileType) => acc + fileType.insertions, 0); - result.deletions = Object.values(result.fileTypesAndContributionMap) + (result as CommitResult).deletions = Object.values(result.fileTypesAndContributionMap) .reduce((acc, fileType) => acc + fileType.deletions, 0); }); }); diff --git a/frontend/src/utils/user.ts b/frontend/src/utils/user.ts index aa755fda53..08fada6e72 100644 --- a/frontend/src/utils/user.ts +++ b/frontend/src/utils/user.ts @@ -1,13 +1,16 @@ -export default class User { +import { Commit, DailyCommit, User as UserType } from '../types/types'; +import { AuthorFileTypeContributions } from '../types/zod/commits-type'; + +export default class User implements UserType { checkedFileTypeContribution : number; - commits: Array; + commits: Commit[]; - dailyCommits: Array; + dailyCommits: DailyCommit[]; displayName: string; - fileTypeContribution: object; + fileTypeContribution: AuthorFileTypeContributions; location: string;