From 2a1a19ffc64eaa0b0b7dd490ba1ca4be49d7a509 Mon Sep 17 00:00:00 2001 From: Josh Kay Date: Thu, 2 Mar 2023 11:52:27 +1300 Subject: [PATCH 01/25] display list of repos states --- src/config/interfaces.ts | 3 +- src/routes/jira/jira-get-connected-repos.ts | 139 ++++++++++++++++++++ src/routes/jira/jira-get.ts | 2 + src/routes/jira/jira-router.ts | 3 + src/util/handlebars/handlebar-helpers.ts | 13 ++ static/css/jira-connected-repos.css | 107 +++++++++++++++ static/js/jira-configuration.js | 14 ++ static/js/jira-connected-repos.js | 8 ++ views/jira-configuration-new.hbs | 2 + views/jira-configuration.hbs | 10 ++ views/jira-connected-repos.hbs | 138 +++++++++++++++++++ views/partials/jira-configuration-table.hbs | 14 +- 12 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 src/routes/jira/jira-get-connected-repos.ts create mode 100644 static/css/jira-connected-repos.css create mode 100644 static/js/jira-connected-repos.js create mode 100644 views/jira-connected-repos.hbs diff --git a/src/config/interfaces.ts b/src/config/interfaces.ts index 65a6782f73..c6e4fd3de0 100644 --- a/src/config/interfaces.ts +++ b/src/config/interfaces.ts @@ -38,6 +38,7 @@ export interface AppInstallation extends Octokit.AppsGetInstallationResponse { syncWarning?: string; totalNumberOfRepos?: number; numberOfSyncedRepos?: number; + subscriptionId?: number, jiraHost: string; } @@ -45,4 +46,4 @@ export interface FailedAppInstallation { error: FailedInstallationError; id: number; deleted: boolean; -} \ No newline at end of file +} diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-get-connected-repos.ts new file mode 100644 index 0000000000..6e0d1a6a46 --- /dev/null +++ b/src/routes/jira/jira-get-connected-repos.ts @@ -0,0 +1,139 @@ +import { NextFunction, Request, Response } from "express"; +import { RepoSyncState } from "~/src/models/reposyncstate"; +import { Subscription, TaskStatus } from "~/src/models/subscription"; + +interface Page { + pageNum: number; + isCurrentPage: boolean; +} + +export const JiraGetConnectedRepos = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + + try { + const { jiraHost, nonce } = res.locals; + const subscriptionId = Number(req.params.subscriptionId) || Number(req.body.subscriptionId); + const page = Number(req.query.page || 1); + + if (!jiraHost) { + req.log.warn({ jiraHost, req, res }, "Missing jiraHost"); + res.status(404).send(`Missing Jira Host`); + return; + } + + if (!subscriptionId) { + req.log.error("Missing Subscription ID"); + res.status(401).send("Missing Subscription ID"); + return; + } + + const subscription = await Subscription.findByPk(subscriptionId); + + const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); + const repos = repoSyncStates.map((repoSyncState) => { + return { + name: repoSyncState.repoFullName, + syncStatus: mapTaskStatus(getSyncStatus(repoSyncState)), + branchStatus: repoSyncState?.branchStatus, + commitStatus: repoSyncState?.commitStatus, + pullStatus: repoSyncState?.pullStatus, + buildStatus: repoSyncState?.buildStatus, + deploymentStatus: repoSyncState?.deploymentStatus + }; + }); + + const pageSize = 10; + const startPageIndex = (page - 1) * pageSize; + const repoPage = repos.slice(startPageIndex, startPageIndex + pageSize); + + res.render("jira-connected-repos.hbs", { + host: jiraHost, + repos: repoPage, + subscriptionId, + csrfToken: req.csrfToken(), + nonce, + ...getPaginationState(page, pageSize, repos) + }); + + } catch (error) { + return next(new Error(`Failed to render connected repos: ${error}`)); + } +}; + +const getPaginationState = (page: number, pageSize: number, repos: any[]) => { + + const totalPages = Math.ceil(repos.length / pageSize); + const hasPrevPage = page > 1; + const prevPageNum = page - 1; + const hasNextPage = page < totalPages; + const nextPageNum = page + 1; + + const pages = getPaginationNumbers(page, totalPages); + + return { + page, + totalPages, + hasPrevPage, + prevPageNum, + hasNextPage, + nextPageNum, + pages + }; +}; + +const getPaginationNumbers = (currentPageNum: number, totalPages: number): Page[] => { + + const maxPagesToShow = 20; + const pages: Page[] = []; + + // Determine the range of pages to show + let startPage = Math.max(currentPageNum - Math.floor(maxPagesToShow / 2), 1); + const endPage = Math.min(startPage + maxPagesToShow - 1, totalPages); + + // Adjust the range so it shows pages relative to current + if (endPage - startPage < maxPagesToShow - 1) { + startPage = Math.max(endPage - maxPagesToShow + 1, 1); + } + + // Add the pages to the array + for (let pageNum = startPage; pageNum <= endPage; pageNum++) { + pages.push({ + pageNum, + isCurrentPage: pageNum === currentPageNum + }); + } + + return pages; +}; + +const getSyncStatus = (repoSyncState: RepoSyncState): TaskStatus => { + + const statuses = [repoSyncState?.branchStatus, repoSyncState?.commitStatus, repoSyncState?.pullStatus, repoSyncState?.buildStatus, repoSyncState?.deploymentStatus]; + if (statuses.includes("pending")) { + return "pending"; + } + if (statuses.includes("failed")) { + return "failed"; + } + const completeStatusesCount = statuses.filter((status) => status == "complete").length; + if (completeStatusesCount === statuses.length) { + return "complete"; + } + return "pending"; +}; + +const mapTaskStatus = (syncStatus: TaskStatus): string => { + switch (syncStatus) { + case "pending": + return "IN PROGRESS"; + case "complete": + return "FINISHED"; + case "failed": + return "FAILED"; + default: + return syncStatus; + } +}; diff --git a/src/routes/jira/jira-get.ts b/src/routes/jira/jira-get.ts index 7e01d1e495..b87d5b2b18 100644 --- a/src/routes/jira/jira-get.ts +++ b/src/routes/jira/jira-get.ts @@ -71,6 +71,7 @@ const getInstallation = async (subscription: Subscription, gitHubAppId: number | const response = await gitHubAppClient.getInstallation(gitHubInstallationId); return { ...response.data, + subscriptionId: subscription.id, syncStatus: mapSyncStatus(subscription.syncStatus), syncWarning: subscription.syncWarning, totalNumberOfRepos: subscription.totalNumberOfRepos, @@ -168,6 +169,7 @@ const renderJiraCloudAndEnterpriseServer = async (res: Response, req: Request): hasCloudServers: !!(successfulCloudConnections.length || failedCloudConnections.length), hasConnections, APP_URL: process.env.APP_URL, + enableRepoConnectedPage: await booleanFlag(BooleanFlags.REPO_CREATED_EVENT, jiraHost), csrfToken: req.csrfToken(), nonce }); diff --git a/src/routes/jira/jira-router.ts b/src/routes/jira/jira-router.ts index 68b834fb4e..81ebd8c1a6 100644 --- a/src/routes/jira/jira-router.ts +++ b/src/routes/jira/jira-router.ts @@ -10,6 +10,7 @@ import { JiraConnectRouter } from "routes/jira/connect/jira-connect-router"; import { body } from "express-validator"; import { returnOnValidationError } from "routes/api/api-utils"; import { jiraSymmetricJwtMiddleware } from "~/src/middleware/jira-symmetric-jwt-middleware"; +import { JiraGetConnectedRepos } from "~/src/routes/jira/jira-get-connected-repos"; export const JiraRouter = Router(); @@ -30,6 +31,8 @@ JiraRouter.use("/events", JiraEventsRouter); JiraRouter.get("/", csrfMiddleware, jiraSymmetricJwtMiddleware, JiraGet); +JiraRouter.get("/subscription/:subscriptionId/repos", csrfMiddleware, jiraSymmetricJwtMiddleware, JiraGetConnectedRepos); + /******************************************************************************************************************** * TODO: remove this later, keeping this for now cause its out in `Prod` * *******************************************************************************************************************/ diff --git a/src/util/handlebars/handlebar-helpers.ts b/src/util/handlebars/handlebar-helpers.ts index a2a7e1264b..4d4d449fab 100644 --- a/src/util/handlebars/handlebar-helpers.ts +++ b/src/util/handlebars/handlebar-helpers.ts @@ -59,4 +59,17 @@ export const registerHandlebarsHelpers = () => { subscriptionHost !== jiraHost ); + const syncStatusIcon = (status) => { + switch (status) { + case "complete": + return "Complete"; + case "failed": + return "Failed"; + default: + return "Incomplete"; + } + + }; + + hbs.registerHelper("syncStatusIcon", syncStatusIcon); }; diff --git a/static/css/jira-connected-repos.css b/static/css/jira-connected-repos.css new file mode 100644 index 0000000000..f849ca399c --- /dev/null +++ b/static/css/jira-connected-repos.css @@ -0,0 +1,107 @@ +.jiraConnectedRepos { + box-sizing: border-box; + height: 100vh; + overflow-y: hidden; + padding: 3.5em 4.5em; +} + +.jiraConnectedRepos__header { + display: flex; + align-items: center; +} + +.jiraConnectedRepos__header__title { + font-weight: 500; + margin-left: 20px; +} + +.jiraConnectedRepos_content { + margin-top: 2em; +} + +.jiraConnectedRepos_content > .jiraConnectedRepos__tableContainer { + overflow: auto; + margin-top: 2.5em; + box-shadow: 0 1px 1px rgb(9 30 66 / 25%), 0 0 1px rgb(9 30 66 / 31%); + border-radius: 12px; + padding: 1.5em 1.5em; +} + +.jiraConnectedRepos_content > .jiraConnectedRepos__tableContainer > .jiraConnectedRepos__table { + color: #172b4d; + table-layout: fixed; + width: 100%; +} + +.jiraConnectedRepos__table__head { + color: #6b778c; + font-size: 0.8rem; +} + +.jiraConnectedRepos__table__head_row > .jiraConnectedRepos__table__head__title { + font-weight: normal; + padding: 0.3em 0; +} + +/*.jiraConnectedRepos__table__head__title:not(:last-child) {*/ +/* width: 30%;*/ +/*}*/ + +.jiraConnectedRepos__table__body +> .jiraConnectedRepos__table__row +> .jiraConnectedRepos__table__cell, +.jiraConnectedRepos__table__body +> .jiraConnectedRepos__table__row +> .jiraConnectedRepos__table__cell__settings { + border-bottom: none; + border-top: none; + padding: 0.8em 0; +} + +.jiraConnectedRepos__table__syncStatus, +.syncStatusPending, +.syncStatusInProgress, +.syncStatusFinished, +.syncStatusFailed { + border-radius: 5px; + font-size: 1rem; + font-weight: bold; + padding: 0.2em 0.3em; +} + +.jiraConnectedRepos__table__pending, +.syncStatusPending { + background-color: #dfe1e6; + color: #42526e; +} + +.jiraConnectedRepos__table__in-progress, +.syncStatusInProgress { + background-color: #deebff; + color: #0747a6; +} + +.jiraConnectedRepos__table__finished, +.syncStatusFinished { + background-color: #e3fcef; + color: #006644; +} + +.jiraConnectedRepos__table__failed, +.syncStatusFailed { + background-color: #ffebe6; + color: #bf2600; +} + +.jiraConnectedRepos__table__cell__syncStatus { + display: flex; +} + +.jiraConnectedRepos__loaderContainer { + margin-left: 8px; +} + +.jiraConnectedRepos_pagination { + margin: 1rem; +} + diff --git a/static/js/jira-configuration.js b/static/js/jira-configuration.js index 0943665b13..9bbc6a7526 100644 --- a/static/js/jira-configuration.js +++ b/static/js/jira-configuration.js @@ -23,6 +23,13 @@ $(".add-organization-link").click(function(event) { }); }); +$(".jiraConfiguration__table__repo_access").click(function (event) { + const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); + AP.context.getToken(function (token) { + window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}`; + }); +}); + $(".add-enterprise-link").click(function(event) { event.preventDefault(); AP.navigator.go( @@ -256,6 +263,13 @@ if (genericModalClose != null) { }); } +$(".jiraConfiguration__table__repo_access").click(function (event) { + const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); + AP.context.getToken(function (token) { + window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}`; + }); +}); + // When the user clicks anywhere outside of the modal, close it window.onclick = function(event) { if (event.target.className === "jiraConfiguration__syncRetryModalOverlay") { diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js new file mode 100644 index 0000000000..ca796ab905 --- /dev/null +++ b/static/js/jira-connected-repos.js @@ -0,0 +1,8 @@ +$(".page-selector").click(function (event) { + const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); + const pageNumber = $(event.target).attr('data-page-num'); + + AP.context.getToken(function (token) { + window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}&page=${pageNumber}`; + }); +}); diff --git a/views/jira-configuration-new.hbs b/views/jira-configuration-new.hbs index e10e21d133..68bca8e63d 100644 --- a/views/jira-configuration-new.hbs +++ b/views/jira-configuration-new.hbs @@ -183,6 +183,7 @@ host=host shouldPromoteBackfillButtonToTableRow=shouldPromoteBackfillButtonToTableRow elementIdPrefix="ghCloud-" + enableRepoConnectedPage=enableRepoConnectedPage html_url=html_url csrfToken=csrfToken successfulConnections=ghCloud.successfulCloudConnections @@ -264,6 +265,7 @@ successfulConnections=app.successfulConnections failedConnections=app.failedConnections gitHubAppId=app.id + enableRepoConnectedPage=enableRepoConnectedPage }} {{else}}
diff --git a/views/jira-configuration.hbs b/views/jira-configuration.hbs index 3df59bf093..890fcb91b9 100644 --- a/views/jira-configuration.hbs +++ b/views/jira-configuration.hbs @@ -172,11 +172,21 @@ {{! Repos Synced }} + {{#if enableRepoConnectedPage}} + + {{#if isGlobalInstall}} + All repos + {{else}} + Only select repos + {{/if}} + + {{else}} {{#if isGlobalInstall}} All repos {{else}} Only select repos {{/if}} + {{/if}} {{#if totalNumberOfRepos}} diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs new file mode 100644 index 0000000000..fec3036850 --- /dev/null +++ b/views/jira-connected-repos.hbs @@ -0,0 +1,138 @@ + + + + + + + + {{title}} + + + + + + + + + +
+
+
+ +
+
+

+ GitHub connected repositories +

+
+
+ +
+ +
+ + + + + + + + + + + + + + + {{#each repos}} + + + {{! Repo name }} + + + {{! Backfill status }} + + + {{! Branch Status }} + + + {{! Commit Status }} + + + {{! Pull Status }} + + + {{! Build Status }} + + + {{! Deployment Status }} + + + + {{/each}} + +
RepositoryBackfillBranchCommitPullBuildDeployment
+ {{name}} + +
+ + {{syncStatus}} + +
+
+ {{{syncStatusIcon branchStatus}}} + + {{{syncStatusIcon commitStatus}}} + + {{{syncStatusIcon pullStatus}}} + + {{{syncStatusIcon buildStatus}}} + + {{{syncStatusIcon deploymentStatus}}} +
+
+
+ {{#if hasPrevPage}} + Prev + {{/if}} + + {{#each pages}} + {{#if isCurrentPage}} + {{pageNum}} + {{else}} + {{pageNum}} + {{/if}} + {{/each}} + + {{#if hasNextPage}} + Next + {{/if}} +
+
+ + + + + + + + +
+ + + + + + + + + + diff --git a/views/partials/jira-configuration-table.hbs b/views/partials/jira-configuration-table.hbs index 2f5b8d4d38..7f6834feae 100644 --- a/views/partials/jira-configuration-table.hbs +++ b/views/partials/jira-configuration-table.hbs @@ -39,10 +39,20 @@ {{! Repos Synced }} - {{#if connection.isGlobalInstall}} - All repos + {{#if ../enableRepoConnectedPage}} + + {{#if connection.isGlobalInstall}} + All repos + {{else}} + Only select repos + {{/if}} + {{else}} + {{#if connection.isGlobalInstall}} + All repos + {{else}} Only select repos + {{/if}} {{/if}} {{#if connection.totalNumberOfRepos}} From 76f86e27cbda1cb190b9f0e22b6c9e242a4d230e Mon Sep 17 00:00:00 2001 From: Josh Kay Date: Fri, 3 Mar 2023 11:36:19 +1300 Subject: [PATCH 02/25] added pagination for repo list --- views/jira-connected-repos.hbs | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index fec3036850..3b252eaf95 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -39,11 +39,6 @@ Repository Backfill - Branch - Commit - Pull - Build - Deployment @@ -68,31 +63,6 @@
- {{! Branch Status }} - - {{{syncStatusIcon branchStatus}}} - - - {{! Commit Status }} - - {{{syncStatusIcon commitStatus}}} - - - {{! Pull Status }} - - {{{syncStatusIcon pullStatus}}} - - - {{! Build Status }} - - {{{syncStatusIcon buildStatus}}} - - - {{! Deployment Status }} - - {{{syncStatusIcon deploymentStatus}}} - - {{/each}} From 80880df60746e36a542dd6a41da3ad50765f4d9b Mon Sep 17 00:00:00 2001 From: Josh Kay Date: Fri, 3 Mar 2023 12:04:06 +1300 Subject: [PATCH 03/25] removed unusd handlebar helper --- src/util/handlebars/handlebar-helpers.ts | 13 ------------- static/css/jira-connected-repos.css | 6 +++--- views/partials/navigation.hbs | 2 +- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/util/handlebars/handlebar-helpers.ts b/src/util/handlebars/handlebar-helpers.ts index 4d4d449fab..a2a7e1264b 100644 --- a/src/util/handlebars/handlebar-helpers.ts +++ b/src/util/handlebars/handlebar-helpers.ts @@ -59,17 +59,4 @@ export const registerHandlebarsHelpers = () => { subscriptionHost !== jiraHost ); - const syncStatusIcon = (status) => { - switch (status) { - case "complete": - return "Complete"; - case "failed": - return "Failed"; - default: - return "Incomplete"; - } - - }; - - hbs.registerHelper("syncStatusIcon", syncStatusIcon); }; diff --git a/static/css/jira-connected-repos.css b/static/css/jira-connected-repos.css index f849ca399c..8f759eacc8 100644 --- a/static/css/jira-connected-repos.css +++ b/static/css/jira-connected-repos.css @@ -43,9 +43,9 @@ padding: 0.3em 0; } -/*.jiraConnectedRepos__table__head__title:not(:last-child) {*/ -/* width: 30%;*/ -/*}*/ +.jiraConnectedRepos__table__head__title:not(:last-child) { + width: 30%; +} .jiraConnectedRepos__table__body > .jiraConnectedRepos__table__row diff --git a/views/partials/navigation.hbs b/views/partials/navigation.hbs index 4e25d93688..aaa0bbd9b1 100644 --- a/views/partials/navigation.hbs +++ b/views/partials/navigation.hbs @@ -2,7 +2,7 @@ {{#unless hideBackButton}} {{/unless}} From b2d546614359ca043388c030ea51d50818662472 Mon Sep 17 00:00:00 2001 From: Josh Kay Date: Tue, 14 Mar 2023 14:20:02 +1300 Subject: [PATCH 04/25] minor tidies on order and view hbs --- src/models/reposyncstate.ts | 6 +++++- src/routes/jira/jira-get-connected-repos.ts | 8 +------- views/jira-connected-repos.hbs | 8 -------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/models/reposyncstate.ts b/src/models/reposyncstate.ts index 87cc419fbc..f2fafa9b71 100644 --- a/src/models/reposyncstate.ts +++ b/src/models/reposyncstate.ts @@ -98,7 +98,11 @@ export class RepoSyncState extends Model { const result = await RepoSyncState.findAll(merge(options, { where: { subscriptionId: subscription.id - } + }, + order: [ + ["syncStatus", "DESC"], + ["id", "ASC"] + ] })); return result || []; } diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-get-connected-repos.ts index 6e0d1a6a46..c45677836a 100644 --- a/src/routes/jira/jira-get-connected-repos.ts +++ b/src/routes/jira/jira-get-connected-repos.ts @@ -15,15 +15,9 @@ export const JiraGetConnectedRepos = async ( try { const { jiraHost, nonce } = res.locals; - const subscriptionId = Number(req.params.subscriptionId) || Number(req.body.subscriptionId); + const subscriptionId = Number(req.params.subscriptionId); const page = Number(req.query.page || 1); - if (!jiraHost) { - req.log.warn({ jiraHost, req, res }, "Missing jiraHost"); - res.status(404).send(`Missing Jira Host`); - return; - } - if (!subscriptionId) { req.log.error("Missing Subscription ID"); res.status(401).send("Missing Subscription ID"); diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index 3b252eaf95..30f71bedfd 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -86,14 +86,6 @@ {{/if}} - - - - - - - - From c13f6c98b58fbc3b80588fb86fc6725a46db7335 Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 6 Apr 2023 16:22:18 +1000 Subject: [PATCH 05/25] Updated pagination, adding filtering and searching --- src/models/reposyncstate.ts | 3 +- src/routes/jira/jira-get-connected-repos.ts | 68 +++++-- static/css/global.css | 13 +- static/css/jira-connected-repos.css | 75 ++++++++ static/js/jira-connected-repos.js | 98 +++++++++- views/jira-connected-repos.hbs | 188 +++++++++++++------- views/partials/navigation.hbs | 2 +- 7 files changed, 366 insertions(+), 81 deletions(-) diff --git a/src/models/reposyncstate.ts b/src/models/reposyncstate.ts index 69e3cd38a3..8a9ba9cd5c 100644 --- a/src/models/reposyncstate.ts +++ b/src/models/reposyncstate.ts @@ -103,7 +103,7 @@ export class RepoSyncState extends Model { return RepoSyncState.create(merge(values, { subscriptionId: subscription.id }), options); } - private static async countSubscriptionRepos(subscription: Subscription, options: CountOptions = {}): Promise { + static async countSubscriptionRepos(subscription: Subscription, options: CountOptions = {}): Promise { return RepoSyncState.count(merge(options, { where: { subscriptionId: subscription.id @@ -125,7 +125,6 @@ export class RepoSyncState extends Model { subscriptionId: subscription.id }, order: [ - ["syncStatus", "DESC"], ["id", "ASC"] ] })); diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-get-connected-repos.ts index c45677836a..a7ce4d922d 100644 --- a/src/routes/jira/jira-get-connected-repos.ts +++ b/src/routes/jira/jira-get-connected-repos.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from "express"; +import { Op } from "sequelize"; import { RepoSyncState } from "~/src/models/reposyncstate"; import { Subscription, TaskStatus } from "~/src/models/subscription"; @@ -17,6 +18,9 @@ export const JiraGetConnectedRepos = async ( const { jiraHost, nonce } = res.locals; const subscriptionId = Number(req.params.subscriptionId); const page = Number(req.query.page || 1); + const pageSize = Number(req.query.pageSize || 10); + const repoName = req.query.repoName || ""; + const syncStatusFilter = req.query.syncStatus || undefined; if (!subscriptionId) { req.log.error("Missing Subscription ID"); @@ -26,7 +30,54 @@ export const JiraGetConnectedRepos = async ( const subscription = await Subscription.findByPk(subscriptionId); - const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); + let syncStatusCondition = {}; + if (syncStatusFilter && syncStatusFilter !== "all") { + syncStatusCondition = { + [Op.or]: [ + { branchStatus: `${syncStatusFilter}` }, + { commitStatus: `${syncStatusFilter}` }, + { pullStatus: `${syncStatusFilter}` }, + { buildStatus: `${syncStatusFilter}` }, + { deploymentStatus: `${syncStatusFilter}` } + ] + }; + } + + const reposCount = await RepoSyncState.countSubscriptionRepos(subscription, { + where: { + [Op.and]: [ + { + repoName: { + [Op.like]: `%${repoName}%` + } + }, + { + ...syncStatusCondition + } + ] + + } + }); + + const offset = page == 1 ? 0 : (page - 1) * pageSize; + + const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription, { + where: { + [Op.and]: [ + { + repoName: { + [Op.like]: `%${repoName}%` + } + }, + { + ...syncStatusCondition + } + ] + + }, + limit: pageSize, + offset + }); const repos = repoSyncStates.map((repoSyncState) => { return { name: repoSyncState.repoFullName, @@ -35,21 +86,18 @@ export const JiraGetConnectedRepos = async ( commitStatus: repoSyncState?.commitStatus, pullStatus: repoSyncState?.pullStatus, buildStatus: repoSyncState?.buildStatus, - deploymentStatus: repoSyncState?.deploymentStatus + deploymentStatus: repoSyncState?.deploymentStatus, + failedCode: repoSyncState.failedCode }; }); - const pageSize = 10; - const startPageIndex = (page - 1) * pageSize; - const repoPage = repos.slice(startPageIndex, startPageIndex + pageSize); - res.render("jira-connected-repos.hbs", { host: jiraHost, - repos: repoPage, + repos: repos, subscriptionId, csrfToken: req.csrfToken(), nonce, - ...getPaginationState(page, pageSize, repos) + ...getPaginationState(page, pageSize, reposCount) }); } catch (error) { @@ -57,9 +105,9 @@ export const JiraGetConnectedRepos = async ( } }; -const getPaginationState = (page: number, pageSize: number, repos: any[]) => { +const getPaginationState = (page: number, pageSize: number, reposCount: number) => { - const totalPages = Math.ceil(repos.length / pageSize); + const totalPages = Math.ceil(reposCount / pageSize); const hasPrevPage = page > 1; const prevPageNum = page - 1; const hasNextPage = page < totalPages; diff --git a/static/css/global.css b/static/css/global.css index b79a6ea088..5be9423c3d 100644 --- a/static/css/global.css +++ b/static/css/global.css @@ -160,6 +160,7 @@ a:focus { .jiraConfiguration__errorSummaryModalOverlay, .jiraConfiguration__restartBackfillModalOverlay, .githubSetup__newJiraSiteModalOverlay, +.jiraConnectedRepos__backfillStatusModalOverlay, .modal__modalOverlay { background: #091e42; opacity: 54%; @@ -169,6 +170,7 @@ a:focus { .jiraConfiguration__syncRetryModal, .jiraConfiguration__restartBackfillModal, .jiraConfiguration__errorSummaryModal, +.jiraConnectedRepos__backfillStatusModal, .githubSetup__newJiraSiteModal, .modal { display: none; @@ -180,6 +182,8 @@ a:focus { .jiraConfiguration__errorSummaryModalOverlay, .jiraConfiguration__restartBackfillModal, .jiraConfiguration__restartBackfillModalOverlay, +.jiraConnectedRepos__backfillStatusModal, +.jiraConnectedRepos__backfillStatusModalOverlay, .githubSetup__newJiraSiteModal, .githubSetup__newJiraSiteModalOverlay, .modal, @@ -194,6 +198,7 @@ a:focus { .jiraConfiguration__syncRetryModalContent, .jiraConfiguration__errorSummaryModalContent, .jiraConfiguration__restartBackfillModalContent, +.jiraConnectedRepos__backfillStatusModalContent, .githubSetup__newJiraSiteModalContent, .modal__modalContent { -webkit-transform: translateX(-50%); @@ -209,11 +214,16 @@ a:focus { .jiraConfiguration__syncRetryModalContent, .jiraConfiguration__errorSummaryModalContent, -.jiraConfiguration__restartBackfillModalContent { +.jiraConfiguration__restartBackfillModalContent{ top: 15%; width: 56%; } +.jiraConnectedRepos__backfillStatusModalContent { + top: 15%; + width: 25%; +} + .jiraConfiguration__restartBackfillModalContent { max-width: 540px; } @@ -230,6 +240,7 @@ a:focus { .jiraConfiguration__syncRetryModalOverlay, .jiraConfiguration__errorSummaryModalOverlay, .jiraConfiguration__restartBackfillModalOverlay, +.jiraConnectedRepos__backfillStatusModalOverlay, .githubSetup__newJiraSiteModalOverlay, .modal__modalOverlay { background: #091e42; diff --git a/static/css/jira-connected-repos.css b/static/css/jira-connected-repos.css index 8f759eacc8..b4ce55ef18 100644 --- a/static/css/jira-connected-repos.css +++ b/static/css/jira-connected-repos.css @@ -103,5 +103,80 @@ .jiraConnectedRepos_pagination { margin: 1rem; + display: flex; + justify-content: center; + font-size: 1.2em; +} + +.jiraConnectedRepos_pagination .page-num-link, +.jiraConnectedRepos_pagination .current-page { + margin: 0px 5px; +} + +.jiraConnectedRepos__actionContainer { + display: flex; + justify-content: flex-end; +} + +.jiraConnectedRepos__action__statusFilter { + margin-right: 15px; +} + +.jiraConnectedRepos__backfillStatusModal__header__container { + display: flex; + justify-content: space-between; + margin-bottom: 1.2em; +} + +.jiraConnectedRepos__backfillStatusModal__closeBtn { + cursor: pointer; + font-size: 20px; +} + +.jiraConfiguration__restartBackfillModal__content__desc { + display: flex; + flex-direction: column; + align-items: center; + color: #344563; + font-size: 1.1rem; + margin-bottom: 1.2em +} + +.jiraConfiguration__restartBackfillModal__content__taskContainer { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.jiraConfiguration__restartBackfillModal__content__taskName { + width: 150px; + font-size: 1.05em; +} + +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-check { + color: blue; + align-items: center; +} + +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-cross { + color: red; + align-items: center; +} + +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-progress { + align-items: center; +} + +.jiraConfiguration__restartBackfillModal__content__backfillTaskStatuses{ + display: flex; + flex-direction: column; + align-items: center; } +.jiraConfiguration__restartBackfillModal__content__ErrorContainer { + display: none; + align-items: center; + margin: 20px 0px; +} + + diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index ca796ab905..ae5dfa23ad 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -1,8 +1,96 @@ -$(".page-selector").click(function (event) { - const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); - const pageNumber = $(event.target).attr('data-page-num'); +$(document).ready(() => { + const params = new URLSearchParams(window.location.search.substring(1)); + const repoSearch = params.get("repoName"); + if (repoSearch) { + $("#repo-search").val(repoSearch); + } + const syncStatus = params.get("syncStatus"); + if(syncStatus) { + $("#status-filter").val(syncStatus); + } - AP.context.getToken(function (token) { - window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}&page=${pageNumber}`; + $(".page-selector").click((event) => { + const pageNumber = $(event.target).attr('data-page-num'); + loadRepos(pageNumber); + }); + + $("#repo-search-btn").click(() => { + loadRepos(1); + }); + + $("#status-filter").on("change", () => { + loadRepos(1); + }); + + const loadRepos = (pageNumber ) => { + const syncStatus =$("#status-filter").val(); + const repoName = $("#repo-search").val(); + const subscriptionId = $(".jiraConnectedRepos_pagination").attr('data-subscription-id'); + AP.context.getToken(function (token) { + window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}&page=${pageNumber}&repoName=${repoName}&syncStatus=${syncStatus}`; + }); + }; + + $(".jiraConnectedRepos__table__cell__repoName").click((event) => { + document.getElementById("backfill-status-modal").style.display = "block"; + const repoName = event.currentTarget.getAttribute("data-repo-name"); + $("#jiraConnectedRepos__backfillStatusModal__header__repoName").text(repoName); + //set branch status icon + const branchStatus = event.currentTarget.getAttribute("data-branch-status"); + const branchStatusIconInfo = getStatusIconInfo(branchStatus); + $(".jiraConfiguration__restartBackfillModal__content__branchStatus span").attr("class", `aui-icon aui-icon-large ${branchStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__branchStatus span").attr("title", branchStatusIconInfo.title); + //set commit status icon + const commitStatus = event.currentTarget.getAttribute("data-commit-status"); + const commitStatusIconInfo = getStatusIconInfo(commitStatus); + $(".jiraConfiguration__restartBackfillModal__content__commitStatus span").attr("class", `aui-icon aui-icon-large ${commitStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__commitStatus span").attr("title", commitStatusIconInfo.title); + //set pull request status icon + const pullRequestStatus = event.currentTarget.getAttribute("data-pull-request-status"); + const pullRequestStatusIconInfo = getStatusIconInfo(pullRequestStatus); + $(".jiraConfiguration__restartBackfillModal__content__pullRequestStatus span").attr("class", `aui-icon aui-icon-large ${pullRequestStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__pullRequestStatus span").attr("title", pullRequestStatusIconInfo.title); + //set build status icon + const buildStatus = event.currentTarget.getAttribute("data-build-status"); + const buildStatusIconInfo = getStatusIconInfo(buildStatus); + $(".jiraConfiguration__restartBackfillModal__content__buildStatus span").attr("class", `aui-icon aui-icon-large ${buildStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__buildStatus span").attr("title", buildStatusIconInfo.title); + //set build status icon + const deploymentStatus = event.currentTarget.getAttribute("data-deployment-status"); + const deploymentStatusIconInfo = getStatusIconInfo(deploymentStatus); + $(".jiraConfiguration__restartBackfillModal__content__deploymentStatus span").attr("class", `aui-icon aui-icon-large ${deploymentStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__deploymentStatus span").attr("title", deploymentStatusIconInfo.title); + //set error reason + const dataFailedCode = event.currentTarget.getAttribute("data-failed-code"); + if(dataFailedCode && dataFailedCode.length > 0) { + $(".jiraConfiguration__restartBackfillModal__content__ErrorContainer").css("display", "flex"); + $(".jiraConfiguration__restartBackfillModal__content__error span").text(mapErrorToMessage(dataFailedCode)); + } else { + $(".jiraConfiguration__restartBackfillModal__content__ErrorContainer").css("display", "none"); + } + }); + + $(".jiraConnectedRepos__backfillStatusModal__closeBtn").click(()=> { + document.getElementById("backfill-status-modal").style.display = "none"; }); }); + +const getStatusIconInfo = (status) => { + if(status == "complete") { + return { cls: "aui-iconfont-check", title: "COMPLETE" } + } + if(status == "failed") { + return { cls: "aui-iconfont-cross", title: "FAILED" } + } + if(status == "pending") { + return { cls: "aui-iconfont-progress", title: "IN PROGRESS" } + } +} + +const mapErrorToMessage = (errorCode) => { + if(errorCode == "CONNECTION_ERROR") { + return "This is caused due to connection error. Restart of backfilling may rectify the error. If error persists, please contact the Atlassian support team." + } + return errorCode; +}; + diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index 30f71bedfd..69228a67d1 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -7,8 +7,8 @@ {{title}} + integrity="DTM1Q+8lU7SzJT+FWr0JFisCSZlwfM0GiAKYy7h1s9vIKa/CIh37s9NuOCqIOgK4tmqrjLK4NuWuIPUQNsikHA==" + crossorigin="anonymous" referrerpolicy="no-referrer" /> @@ -17,84 +17,148 @@ -
-
-
- +
+
+
+ +
+
+

+ GitHub connected repositories +

+
-
-

- GitHub connected repositories -

-
-
-
+
+
+
+
+ +
+ + +
+
+
+ + + + + + + -
-
RepositoryBackfill Status
- - - - - - + + {{#each repos}} + - - {{#each repos}} - + {{! Repo name }} + - {{! Repo name }} - - - {{! Backfill status }} - + {{syncStatus}} + + + - - {{/each}} - -
RepositoryBackfill
+ + {{name}} + + - {{name}} - -
- +
+ - {{syncStatus}} - -
-
-
-
- {{#if hasPrevPage}} - Prev - {{/if}} + + {{/each}} + + +
+
+ {{#if hasPrevPage}} + Prev + {{/if}} - {{#each pages}} + {{#each pages}} {{#if isCurrentPage}} {{pageNum}} {{else}} {{pageNum}} {{/if}} - {{/each}} + {{/each}} + + {{#if hasNextPage}} + Next + {{/if}} +
+
- {{#if hasNextPage}} - Next - {{/if}} + +
+
+
+

+ +
+
+
You can find the status of each + backfill task below
+
+
+
Branches
+
+
+
+
+
Commits
+
+
+
+
+
Pull Requests
+
+
+
+
+
Builds
+
+
+
+
+
Deployments
+
+
+
+
+
+
Reason for failure -
+
+
+
+
-
-
+
- - - - - + + + + - + - + \ No newline at end of file diff --git a/views/partials/navigation.hbs b/views/partials/navigation.hbs index aaa0bbd9b1..4e25d93688 100644 --- a/views/partials/navigation.hbs +++ b/views/partials/navigation.hbs @@ -2,7 +2,7 @@ {{#unless hideBackButton}} {{/unless}} From 4bbec838522f2dc6b92d932c53eef8dc7e8818c3 Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 6 Apr 2023 16:25:58 +1000 Subject: [PATCH 06/25] update message --- static/js/jira-connected-repos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index ae5dfa23ad..309fac218a 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -89,7 +89,7 @@ const getStatusIconInfo = (status) => { const mapErrorToMessage = (errorCode) => { if(errorCode == "CONNECTION_ERROR") { - return "This is caused due to connection error. Restart of backfilling may rectify the error. If error persists, please contact the Atlassian support team." + return "This is caused due to the connection error. Restart of backfilling may rectify the error. If error persists, please contact the Atlassian support team." } return errorCode; }; From 49b8ed7a43a40a9cb6f58deb5d0cf6f11cd28631 Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 29 Jun 2023 11:36:22 +1000 Subject: [PATCH 07/25] updated UI as per specs --- src/config/feature-flags.ts | 3 +- src/routes/jira/jira-get-connected-repos.ts | 6 + src/routes/jira/jira-get.ts | 3 +- static/css/global.css | 2 +- static/css/jira-connected-repos.css | 91 +++++++++-- static/js/jira-connected-repos.js | 20 +-- views/jira-configuration.hbs | 162 +------------------- views/jira-connected-repos.hbs | 26 ++-- 8 files changed, 115 insertions(+), 198 deletions(-) diff --git a/src/config/feature-flags.ts b/src/config/feature-flags.ts index 324f04ce64..68b1b00113 100644 --- a/src/config/feature-flags.ts +++ b/src/config/feature-flags.ts @@ -25,7 +25,8 @@ export enum BooleanFlags { LOG_CURLV_OUTPUT = "log-curlv-output", SKIP_REQUESTED_REVIEWERS = "skip-requested-reviewers", ENABLE_SUBSCRIPTION_DEFERRED_INSTALL = "enable-subscription-deferred-install", - USE_REST_API_FOR_DISCOVERY = "use-rest-api-for-discovery-again" + USE_REST_API_FOR_DISCOVERY = "use-rest-api-for-discovery-again", + ENABLE_CONNECTED_REPOS_VIEW="enable-connected-repos-view" } export enum StringFlags { diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-get-connected-repos.ts index a7ce4d922d..b64f639647 100644 --- a/src/routes/jira/jira-get-connected-repos.ts +++ b/src/routes/jira/jira-get-connected-repos.ts @@ -30,6 +30,12 @@ export const JiraGetConnectedRepos = async ( const subscription = await Subscription.findByPk(subscriptionId); + if (!subscription) { + req.log.error("Missing Subscription"); + res.status(401).send("Missing Subscription"); + return; + } + let syncStatusCondition = {}; if (syncStatusFilter && syncStatusFilter !== "all") { syncStatusCondition = { diff --git a/src/routes/jira/jira-get.ts b/src/routes/jira/jira-get.ts index 895ec39891..c3e5b22467 100644 --- a/src/routes/jira/jira-get.ts +++ b/src/routes/jira/jira-get.ts @@ -12,6 +12,7 @@ import { sendAnalytics } from "utils/analytics-client"; import { AnalyticsEventTypes, AnalyticsScreenEventsEnum } from "interfaces/common"; import { getCloudOrServerFromGitHubAppId } from "utils/get-cloud-or-server"; import { Errors } from "config/errors"; +import { BooleanFlags, booleanFlag } from "~/src/config/feature-flags"; interface FailedConnection { id: number; @@ -169,7 +170,7 @@ const renderJiraCloudAndEnterpriseServer = async (res: Response, req: Request): hasCloudServers: !!(successfulCloudConnections.length || failedCloudConnections.length), hasConnections, APP_URL: process.env.APP_URL, - enableRepoConnectedPage: await booleanFlag(BooleanFlags.REPO_CREATED_EVENT, jiraHost), + enableRepoConnectedPage: await booleanFlag(BooleanFlags.ENABLE_CONNECTED_REPOS_VIEW, jiraHost), csrfToken: req.csrfToken(), nonce }); diff --git a/static/css/global.css b/static/css/global.css index 5be9423c3d..2cc434e348 100644 --- a/static/css/global.css +++ b/static/css/global.css @@ -221,7 +221,7 @@ a:focus { .jiraConnectedRepos__backfillStatusModalContent { top: 15%; - width: 25%; + width: 550px; } .jiraConfiguration__restartBackfillModalContent { diff --git a/static/css/jira-connected-repos.css b/static/css/jira-connected-repos.css index b4ce55ef18..4bec4fa5cf 100644 --- a/static/css/jira-connected-repos.css +++ b/static/css/jira-connected-repos.css @@ -105,22 +105,67 @@ margin: 1rem; display: flex; justify-content: center; - font-size: 1.2em; + align-items: center; + color: #42526e; +} + +.jiraConnectedRepos_pagination .page-num-link { + margin: 0px 12px; + cursor: pointer; } -.jiraConnectedRepos_pagination .page-num-link, .jiraConnectedRepos_pagination .current-page { - margin: 0px 5px; + background: #253858; + width: 32px; + height: 32px; + color: #F4F5F7; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; +} + +.jiraConnectedRepos_pagination .next-page { + margin-left: 10px; +} + +.jiraConnectedRepos_pagination .prev-page { + margin-right: 10px; +} + +.jiraConnectedRepos_pagination .next-page span, +.jiraConnectedRepos_pagination .prev-page span { + --aui-icon-size: 20px; + color:#a5adba; +} + +.jiraConnectedRepos_pagination .next-page.page-selector span, +.jiraConnectedRepos_pagination .prev-page.page-selector span { + cursor: pointer; + color: #42526e; } .jiraConnectedRepos__actionContainer { display: flex; - justify-content: flex-end; + justify-content: flex-start; } .jiraConnectedRepos__action__statusFilter { margin-right: 15px; } +.jiraConnectedRepos__action__search { + position: relative; + width: 250px; + margin-right: 10px; +} + +.jiraConnectedRepos__action__search .aui-iconfont-search { + position: absolute; + right: 20px; + top: 8px; + cursor: pointer; + color: #000000a6; +} .jiraConnectedRepos__backfillStatusModal__header__container { display: flex; @@ -136,9 +181,8 @@ .jiraConfiguration__restartBackfillModal__content__desc { display: flex; flex-direction: column; - align-items: center; color: #344563; - font-size: 1.1rem; + font-size: 1rem; margin-bottom: 1.2em } @@ -150,33 +194,50 @@ .jiraConfiguration__restartBackfillModal__content__taskName { width: 150px; - font-size: 1.05em; + font-weight: 600; + font-size: 1.15em; } -.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-check { - color: blue; +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-approve { + color: #36B37E; align-items: center; + --aui-icon-size: 18px; } -.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-cross { - color: red; +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-cross-circle { + color: #FF5630; align-items: center; + --aui-icon-size: 18px; } -.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-progress { +.jiraConfiguration__restartBackfillModal__content__taskContainer span.aui-iconfont-recent-filled { + color:#0065FF; align-items: center; + --aui-icon-size: 18px; } .jiraConfiguration__restartBackfillModal__content__backfillTaskStatuses{ display: flex; flex-direction: column; - align-items: center; } .jiraConfiguration__restartBackfillModal__content__ErrorContainer { display: none; - align-items: center; - margin: 20px 0px; + margin: 10px 0px; + flex-direction: column; +} + +.jiraConfiguration__restartBackfillModal__content__error { + font-weight: 600; + margin-bottom: 5px; +} + +.jiraConfiguration__restartBackfillModal__content__divider { + width: 100%; + border: 1px solid #091E4224; + margin-bottom: 15px; } + + diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index 309fac218a..82fb10cd26 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -38,33 +38,33 @@ $(document).ready(() => { //set branch status icon const branchStatus = event.currentTarget.getAttribute("data-branch-status"); const branchStatusIconInfo = getStatusIconInfo(branchStatus); - $(".jiraConfiguration__restartBackfillModal__content__branchStatus span").attr("class", `aui-icon aui-icon-large ${branchStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__branchStatus span").attr("class", `aui-icon ${branchStatusIconInfo.cls}`); $(".jiraConfiguration__restartBackfillModal__content__branchStatus span").attr("title", branchStatusIconInfo.title); //set commit status icon const commitStatus = event.currentTarget.getAttribute("data-commit-status"); const commitStatusIconInfo = getStatusIconInfo(commitStatus); - $(".jiraConfiguration__restartBackfillModal__content__commitStatus span").attr("class", `aui-icon aui-icon-large ${commitStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__commitStatus span").attr("class", `aui-icon ${commitStatusIconInfo.cls}`); $(".jiraConfiguration__restartBackfillModal__content__commitStatus span").attr("title", commitStatusIconInfo.title); //set pull request status icon const pullRequestStatus = event.currentTarget.getAttribute("data-pull-request-status"); const pullRequestStatusIconInfo = getStatusIconInfo(pullRequestStatus); - $(".jiraConfiguration__restartBackfillModal__content__pullRequestStatus span").attr("class", `aui-icon aui-icon-large ${pullRequestStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__pullRequestStatus span").attr("class", `aui-icon ${pullRequestStatusIconInfo.cls}`); $(".jiraConfiguration__restartBackfillModal__content__pullRequestStatus span").attr("title", pullRequestStatusIconInfo.title); //set build status icon const buildStatus = event.currentTarget.getAttribute("data-build-status"); const buildStatusIconInfo = getStatusIconInfo(buildStatus); - $(".jiraConfiguration__restartBackfillModal__content__buildStatus span").attr("class", `aui-icon aui-icon-large ${buildStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__buildStatus span").attr("class", `aui-icon ${buildStatusIconInfo.cls}`); $(".jiraConfiguration__restartBackfillModal__content__buildStatus span").attr("title", buildStatusIconInfo.title); //set build status icon const deploymentStatus = event.currentTarget.getAttribute("data-deployment-status"); const deploymentStatusIconInfo = getStatusIconInfo(deploymentStatus); - $(".jiraConfiguration__restartBackfillModal__content__deploymentStatus span").attr("class", `aui-icon aui-icon-large ${deploymentStatusIconInfo.cls}`); + $(".jiraConfiguration__restartBackfillModal__content__deploymentStatus span").attr("class", `aui-icon ${deploymentStatusIconInfo.cls}`); $(".jiraConfiguration__restartBackfillModal__content__deploymentStatus span").attr("title", deploymentStatusIconInfo.title); //set error reason const dataFailedCode = event.currentTarget.getAttribute("data-failed-code"); if(dataFailedCode && dataFailedCode.length > 0) { $(".jiraConfiguration__restartBackfillModal__content__ErrorContainer").css("display", "flex"); - $(".jiraConfiguration__restartBackfillModal__content__error span").text(mapErrorToMessage(dataFailedCode)); + $(".jiraConfiguration__restartBackfillModal__content__errorReason").text(mapErrorToMessage(dataFailedCode)); } else { $(".jiraConfiguration__restartBackfillModal__content__ErrorContainer").css("display", "none"); } @@ -77,19 +77,19 @@ $(document).ready(() => { const getStatusIconInfo = (status) => { if(status == "complete") { - return { cls: "aui-iconfont-check", title: "COMPLETE" } + return { cls: "aui-iconfont-approve", title: "COMPLETE" } } if(status == "failed") { - return { cls: "aui-iconfont-cross", title: "FAILED" } + return { cls: "aui-iconfont-cross-circle", title: "FAILED" } } if(status == "pending") { - return { cls: "aui-iconfont-progress", title: "IN PROGRESS" } + return { cls: "aui-iconfont-recent-filled", title: "IN PROGRESS" } } } const mapErrorToMessage = (errorCode) => { if(errorCode == "CONNECTION_ERROR") { - return "This is caused due to the connection error. Restart of backfilling may rectify the error. If error persists, please contact the Atlassian support team." + return "This is caused by connection error. To fix this, restarting backfilling may rectify this error. If this error persists, please contact the Atlassian support team." } return errorCode; }; diff --git a/views/jira-configuration.hbs b/views/jira-configuration.hbs index 807aa96534..5d24ef9890 100644 --- a/views/jira-configuration.hbs +++ b/views/jira-configuration.hbs @@ -165,166 +165,6 @@
- - {{#if successfulConnections}} -
- - - - - - - - - - - - {{#each successfulConnections}} - - {{! Organization }} - - - {{! Repos Synced }} - - - {{! Backfill status }} - - {{! Settings }} - - - {{/each}} - -
Connected organizationRepository accessBackfill status - - Information - - Settings
- - {{account.login}} - - {{#if enableRepoConnectedPage}} - - {{#if isGlobalInstall}} - All repos - {{else}} - Only select repos - {{/if}} - - {{else}} - {{#if isGlobalInstall}} - All repos - {{else}} - Only select repos - {{/if}} - {{/if}} - - {{#if totalNumberOfRepos}} - - {{ifAllReposSynced - numberOfSyncedRepos - totalNumberOfRepos - }} - - {{/if}} - - - Edit - - - - - {{syncStatus}} - - {{!-- Display any sync warnings --}} - {{#if (inProgressOrPendingSync syncStatus)}} -
- -
- {{/if}} - {{#if (failedSync syncStatus)}} -
- - -
Retry
-
- {{/if}} -
- - - -
- Organization -
- - - Configure - -
- - -
- -
-
-
-
{{#if hasCloudAndEnterpriseServers}}
@@ -431,6 +272,7 @@ successfulConnections=app.successfulConnections failedConnections=app.failedConnections gitHubAppId=app.id + enableRepoConnectedPage=enableRepoConnectedPage }} {{else}}
diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index 69228a67d1..f3a95c4a65 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -34,6 +34,10 @@
+
- -
@@ -88,19 +89,23 @@
{{#if hasPrevPage}} - Prev +
+ {{else}} +
{{/if}} {{#each pages}} {{#if isCurrentPage}} - {{pageNum}} +
{{pageNum}}
{{else}} - {{pageNum}} + {{/if}} {{/each}} {{#if hasNextPage}} - Next +
+ {{else}} +
{{/if}}
@@ -114,8 +119,7 @@ class="jiraConnectedRepos__backfillStatusModal__closeBtn aui-icon aui-iconfont-close-dialog">
-
You can find the status of each - backfill task below
+
Here are the following statuses of each backfill task within this repository.
Branches
@@ -144,7 +148,9 @@
-
Reason for failure -
+
+
Reason for failure
+
From 05c5d39fad1496121a8879c4c108e7818c21efd8 Mon Sep 17 00:00:00 2001 From: Harminder Date: Fri, 30 Jun 2023 13:28:24 +1000 Subject: [PATCH 08/25] fix unit testing --- src/models/reposyncstate.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/models/reposyncstate.ts b/src/models/reposyncstate.ts index 45edeca5d4..5345ca53fc 100644 --- a/src/models/reposyncstate.ts +++ b/src/models/reposyncstate.ts @@ -195,10 +195,7 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { const result = await RepoSyncState.findAll(merge(options, { where: { subscriptionId: subscription.id - }, - order: [ - ["id", "ASC"] - ] + } })); return result || []; } From 9b0b23efe9644e19bec8e37f6748319de68ffb34 Mon Sep 17 00:00:00 2001 From: Harminder Date: Fri, 30 Jun 2023 14:26:06 +1000 Subject: [PATCH 09/25] Fixed unit testing --- test/snapshots/app.test.ts.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index 982457fd13..6c1e0a7fd0 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -169,6 +169,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,_cookieSession,jiraSymmetricJwtMiddleware,JiraWorkspacesRepositoriesAssociate :GET ^/?(?=/|$)^/jira/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,_cookieSession,csrf,jiraSymmetricJwtMiddleware,jiraAdminPermissionsMiddleware,JiraGet +:GET ^/?(?=/|$)^/jira/?(?=/|$)^/subscription/(?:([^/]+?))/repos/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,_cookieSession,csrf,jiraSymmetricJwtMiddleware,JiraGetConnectedRepos :GET ^/?(?=/|$)^/jira/?(?=/|$)^/configuration/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,_cookieSession,csrf,jiraSymmetricJwtMiddleware,jiraAdminPermissionsMiddleware,JiraGet :DELETE ^/?(?=/|$)^/jira/?(?=/|$)^/configuration/?(?=/|$)^/?$ From 2d63fad930294572511a0a7420931e82ea70c0dc Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Mon, 21 Aug 2023 09:22:42 +1000 Subject: [PATCH 10/25] chore: addons and fixes make searchbar not case sensitive re-render repo list on search clear add debouncing to search add FINISHED option in filter --- src/routes/jira/jira-get-connected-repos.ts | 4 ++-- static/js/jira-connected-repos.js | 14 ++++++++++++++ views/jira-connected-repos.hbs | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-get-connected-repos.ts index b64f639647..24e8ba371b 100644 --- a/src/routes/jira/jira-get-connected-repos.ts +++ b/src/routes/jira/jira-get-connected-repos.ts @@ -54,7 +54,7 @@ export const JiraGetConnectedRepos = async ( [Op.and]: [ { repoName: { - [Op.like]: `%${repoName}%` + [Op.iLike]: `%${repoName}%` } }, { @@ -72,7 +72,7 @@ export const JiraGetConnectedRepos = async ( [Op.and]: [ { repoName: { - [Op.like]: `%${repoName}%` + [Op.iLike]: `%${repoName}%` } }, { diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index 82fb10cd26..a9d6b20b19 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -1,4 +1,6 @@ $(document).ready(() => { + // to get the focus back on seach bar after comp reload + $('#repo-search').focus(); const params = new URLSearchParams(window.location.search.substring(1)); const repoSearch = params.get("repoName"); if (repoSearch) { @@ -21,6 +23,18 @@ $(document).ready(() => { $("#status-filter").on("change", () => { loadRepos(1); }); + let repoSearchTimeoutId; + $('#repo-search').on('input', function() { + // re-render the original list after clearing the search bar + if($(this).val().length === 0) { + loadRepos(1) + } + // search bar is using de-bouncing + clearTimeout(repoSearchTimeoutId); + repoSearchTimeoutId = setTimeout(function() { + loadRepos(1); + }, 500); + }); const loadRepos = (pageNumber ) => { const syncStatus =$("#status-filter").val(); diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index f3a95c4a65..d2b04f9b41 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -43,6 +43,7 @@ + From 896da53d5afc461788e377c1cfc0eb155f76c795 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Mon, 21 Aug 2023 16:02:37 +1000 Subject: [PATCH 11/25] chore: add test cases for repo view --- .../jira/jira-get-connected-repos.test.ts | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/routes/jira/jira-get-connected-repos.test.ts diff --git a/src/routes/jira/jira-get-connected-repos.test.ts b/src/routes/jira/jira-get-connected-repos.test.ts new file mode 100644 index 0000000000..756ed1b001 --- /dev/null +++ b/src/routes/jira/jira-get-connected-repos.test.ts @@ -0,0 +1,209 @@ +import { Request, Response, NextFunction } from "express"; +import { Subscription } from "~/src/models/subscription"; +import { RepoSyncState } from "~/src/models/reposyncstate"; +import { Op } from "sequelize"; +import { JiraGetConnectedRepos } from "./jira-get-connected-repos"; +jest.mock("~/src/models/subscription"); +jest.mock("~/src/models/reposyncstate"); + +describe("JiraGetConnectedRepos", () => { + let req: Request; + let res: Response; + let next: NextFunction; + let repo; + + beforeEach(async () => { + req = { + params: { subscriptionId: '111222333' }, + log: { error: jest.fn() }, + query:{ + page: 1, + pageSize: 3, + repoName: 'github-for-jira', + syncStatus: 'finished' + } + } as unknown as Request; + res = { + status: jest.fn(() => res), + send: jest.fn(), + render: jest.fn().mockReturnValue({}), + locals: { + jiraHost: null, + nonce: null + } + } as unknown as Response; + next = jest.fn(); + repo = { + subscriptionId: '111222333', + repoId: 1, + repoName: "github-for-jira", + repoOwner: "atlassian", + repoFullName: "atlassian/github-for-jira", + repoUrl: "github.com/atlassian/github-for-jira" + }; + + }); + afterEach(() => { + jest.resetAllMocks(); + }); + it("should handle missing subscription ID", async () => { + const request = {...req, + params:{ + ...req.params, + subscriptionId: null + } + } as unknown as Request; + await JiraGetConnectedRepos(request, res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(req.log.error).toHaveBeenCalledWith("Missing Subscription ID"); + expect(res.status).toHaveBeenCalledTimes(1); + expect(res.send).toHaveBeenCalledWith("Missing Subscription ID"); + expect(Subscription.findByPk).not.toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + }); + it("should call the error middleware", async () => { + let nullRes= { + status: jest.fn(() => res) + } as unknown as Response;; + await JiraGetConnectedRepos(req, nullRes, next); + expect(next).toBeCalledTimes(1); + }); + it("should handle missing subscription", async () => { + Subscription.findByPk = jest.fn().mockResolvedValueOnce(null); + await JiraGetConnectedRepos(req, res, next); + expect(req.log.error).toHaveBeenCalledTimes(1); + expect(req.log.error).toHaveBeenCalledWith("Missing Subscription"); + expect(res.status).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.send).toHaveBeenCalledTimes(1); + expect(res.send).toHaveBeenCalledWith("Missing Subscription"); + expect(Subscription.findByPk).toHaveBeenCalledTimes(1); + expect(Subscription.findByPk).toHaveBeenCalledWith(111222333); + expect(next).not.toHaveBeenCalled(); + }); + it("should handle error", async () => { + Subscription.findByPk = jest + .fn() + .mockRejectedValueOnce(new Error("something went wrong")); + await JiraGetConnectedRepos(req, res, next); + expect(res.status).not.toHaveBeenCalled(); + expect(res.send).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith( + new Error("Failed to render connected repos: Error: something went wrong") + ); + }); + + it('should call RepoSyncState.countSubscriptionRepos() with the correct arguments', async () => { + const countSubscriptionRepos = jest.spyOn(RepoSyncState, 'countSubscriptionRepos').mockResolvedValueOnce(15); + const subscription = { id: 1 }; + Subscription.findByPk = jest.fn().mockResolvedValueOnce(subscription); + + await JiraGetConnectedRepos(req, res, next); + + expect(countSubscriptionRepos).toHaveBeenCalledWith(subscription, { + where: { + [Op.and]: [ + { + repoName: { + [Op.iLike]: '%github-for-jira%' + } + }, + { + [Op.or]:[ + { + branchStatus: 'finished' + }, + { + commitStatus: 'finished' + }, + { + pullStatus: 'finished' + }, + { + buildStatus: 'finished' + }, + { + deploymentStatus: 'finished' + } + ] + } + ] + } + }); + }); + + it('should call RepoSyncState.findAllFromSubscription() with the correct arguments', async () => { + const reposyncState = [{ + ...repo, + pullStatus: "pending", + commitStatus: "complete", + branchStatus: "pending", + buildStatus: "complete", + deploymentStatus: "complete" + }, + { + ...repo, + pullStatus: "complete", + commitStatus: "failed", + branchStatus: "complete", + buildStatus: "failed", + deploymentStatus: "complete" + }, + { + ...repo, + pullStatus: "complete", + commitStatus: "complete", + branchStatus: "complete", + buildStatus: "complete", + deploymentStatus: "complete" + }]; + const findAllFromSubscription = jest.spyOn(RepoSyncState, 'findAllFromSubscription').mockResolvedValueOnce(Promise.resolve(reposyncState)); + const subscription = { id: 1 }; + Subscription.findByPk = jest.fn().mockResolvedValueOnce(subscription); + + await JiraGetConnectedRepos(req, res, next); + + expect(findAllFromSubscription).toHaveBeenCalledWith(subscription, { + limit: 3, + offset: 0, + where: { + [Op.and]: [ + { + repoName: { + [Op.iLike]: '%github-for-jira%' + } + }, + { + [Op.or]:[ + { + branchStatus: 'finished' + }, + { + commitStatus: 'finished' + }, + { + pullStatus: 'finished' + }, + { + buildStatus: 'finished' + }, + { + deploymentStatus: 'finished' + } + ] + } + ] + } + }); + }); +}); + + + + + + + + + From 659c9df776edff2d2396298bb3f2c1aafdf35246 Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Mon, 21 Aug 2023 16:03:38 +1000 Subject: [PATCH 12/25] chore: add test cases for repo view --- .../jira/jira-get-connected-repos.test.ts | 226 +++++++++--------- 1 file changed, 108 insertions(+), 118 deletions(-) diff --git a/src/routes/jira/jira-get-connected-repos.test.ts b/src/routes/jira/jira-get-connected-repos.test.ts index 756ed1b001..797963fe06 100644 --- a/src/routes/jira/jira-get-connected-repos.test.ts +++ b/src/routes/jira/jira-get-connected-repos.test.ts @@ -7,95 +7,90 @@ jest.mock("~/src/models/subscription"); jest.mock("~/src/models/reposyncstate"); describe("JiraGetConnectedRepos", () => { - let req: Request; - let res: Response; - let next: NextFunction; + let req: Request; + let res: Response; + let next: NextFunction; let repo; - beforeEach(async () => { - req = { - params: { subscriptionId: '111222333' }, - log: { error: jest.fn() }, - query:{ + beforeEach(async () => { + req = { + params: { subscriptionId: "111222333" }, + log: { error: jest.fn() }, + query: { page: 1, pageSize: 3, - repoName: 'github-for-jira', - syncStatus: 'finished' + repoName: "github-for-jira", + syncStatus: "finished" } - } as unknown as Request; - res = { - status: jest.fn(() => res), + } as unknown as Request; + res = { + status: jest.fn(() => res), send: jest.fn(), render: jest.fn().mockReturnValue({}), locals: { - jiraHost: null, + jiraHost: null, nonce: null } - } as unknown as Response; - next = jest.fn(); + } as unknown as Response; + next = jest.fn(); repo = { - subscriptionId: '111222333', + subscriptionId: "111222333", repoId: 1, repoName: "github-for-jira", repoOwner: "atlassian", repoFullName: "atlassian/github-for-jira", repoUrl: "github.com/atlassian/github-for-jira" }; - - }); - afterEach(() => { - jest.resetAllMocks(); - }); - it("should handle missing subscription ID", async () => { - const request = {...req, - params:{ + }); + afterEach(() => { + jest.resetAllMocks(); + }); + it("should handle missing subscription ID", async () => { + const request = { + ...req, + params: { ...req.params, subscriptionId: null } } as unknown as Request; - await JiraGetConnectedRepos(request, res, next); + await JiraGetConnectedRepos(request, res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(req.log.error).toHaveBeenCalledWith("Missing Subscription ID"); + expect(res.status).toHaveBeenCalledTimes(1); + expect(res.send).toHaveBeenCalledWith("Missing Subscription ID"); + expect(Subscription.findByPk).not.toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + }); + it("should handle missing subscription", async () => { + Subscription.findByPk = jest.fn().mockResolvedValueOnce(null); + await JiraGetConnectedRepos(req, res, next); + expect(req.log.error).toHaveBeenCalledTimes(1); + expect(req.log.error).toHaveBeenCalledWith("Missing Subscription"); + expect(res.status).toHaveBeenCalledTimes(1); expect(res.status).toHaveBeenCalledWith(401); - expect(req.log.error).toHaveBeenCalledWith("Missing Subscription ID"); - expect(res.status).toHaveBeenCalledTimes(1); - expect(res.send).toHaveBeenCalledWith("Missing Subscription ID"); - expect(Subscription.findByPk).not.toHaveBeenCalled(); - expect(next).not.toHaveBeenCalled(); - }); - it("should call the error middleware", async () => { - let nullRes= { - status: jest.fn(() => res) - } as unknown as Response;; - await JiraGetConnectedRepos(req, nullRes, next); - expect(next).toBeCalledTimes(1); - }); - it("should handle missing subscription", async () => { - Subscription.findByPk = jest.fn().mockResolvedValueOnce(null); - await JiraGetConnectedRepos(req, res, next); - expect(req.log.error).toHaveBeenCalledTimes(1); - expect(req.log.error).toHaveBeenCalledWith("Missing Subscription"); - expect(res.status).toHaveBeenCalledTimes(1); - expect(res.status).toHaveBeenCalledWith(401); - expect(res.send).toHaveBeenCalledTimes(1); - expect(res.send).toHaveBeenCalledWith("Missing Subscription"); - expect(Subscription.findByPk).toHaveBeenCalledTimes(1); - expect(Subscription.findByPk).toHaveBeenCalledWith(111222333); - expect(next).not.toHaveBeenCalled(); - }); - it("should handle error", async () => { - Subscription.findByPk = jest - .fn() - .mockRejectedValueOnce(new Error("something went wrong")); - await JiraGetConnectedRepos(req, res, next); - expect(res.status).not.toHaveBeenCalled(); - expect(res.send).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith( - new Error("Failed to render connected repos: Error: something went wrong") - ); - }); + expect(res.send).toHaveBeenCalledTimes(1); + expect(res.send).toHaveBeenCalledWith("Missing Subscription"); + expect(Subscription.findByPk).toHaveBeenCalledTimes(1); + expect(Subscription.findByPk).toHaveBeenCalledWith(111222333); + expect(next).not.toHaveBeenCalled(); + }); + it("should handle error", async () => { + Subscription.findByPk = jest + .fn() + .mockRejectedValueOnce(new Error("something went wrong")); + await JiraGetConnectedRepos(req, res, next); + expect(res.status).not.toHaveBeenCalled(); + expect(res.send).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith( + new Error("Failed to render connected repos: Error: something went wrong") + ); + }); - it('should call RepoSyncState.countSubscriptionRepos() with the correct arguments', async () => { - const countSubscriptionRepos = jest.spyOn(RepoSyncState, 'countSubscriptionRepos').mockResolvedValueOnce(15); + it("should call RepoSyncState.countSubscriptionRepos() with the correct arguments", async () => { + const countSubscriptionRepos = jest + .spyOn(RepoSyncState, "countSubscriptionRepos") + .mockResolvedValueOnce(15); const subscription = { id: 1 }; Subscription.findByPk = jest.fn().mockResolvedValueOnce(subscription); @@ -106,59 +101,63 @@ describe("JiraGetConnectedRepos", () => { [Op.and]: [ { repoName: { - [Op.iLike]: '%github-for-jira%' + [Op.iLike]: "%github-for-jira%" } }, { - [Op.or]:[ + [Op.or]: [ { - branchStatus: 'finished' + branchStatus: "finished" }, { - commitStatus: 'finished' + commitStatus: "finished" }, { - pullStatus: 'finished' + pullStatus: "finished" }, { - buildStatus: 'finished' + buildStatus: "finished" }, { - deploymentStatus: 'finished' + deploymentStatus: "finished" } - ] + ] } ] } }); }); - it('should call RepoSyncState.findAllFromSubscription() with the correct arguments', async () => { - const reposyncState = [{ - ...repo, - pullStatus: "pending", - commitStatus: "complete", - branchStatus: "pending", - buildStatus: "complete", - deploymentStatus: "complete" - }, - { - ...repo, - pullStatus: "complete", - commitStatus: "failed", - branchStatus: "complete", - buildStatus: "failed", - deploymentStatus: "complete" - }, - { - ...repo, - pullStatus: "complete", - commitStatus: "complete", - branchStatus: "complete", - buildStatus: "complete", - deploymentStatus: "complete" - }]; - const findAllFromSubscription = jest.spyOn(RepoSyncState, 'findAllFromSubscription').mockResolvedValueOnce(Promise.resolve(reposyncState)); + it("should call RepoSyncState.findAllFromSubscription() with the correct arguments", async () => { + const reposyncState = [ + { + ...repo, + pullStatus: "pending", + commitStatus: "complete", + branchStatus: "pending", + buildStatus: "complete", + deploymentStatus: "complete" + }, + { + ...repo, + pullStatus: "complete", + commitStatus: "failed", + branchStatus: "complete", + buildStatus: "failed", + deploymentStatus: "complete" + }, + { + ...repo, + pullStatus: "complete", + commitStatus: "complete", + branchStatus: "complete", + buildStatus: "complete", + deploymentStatus: "complete" + } + ]; + const findAllFromSubscription = jest + .spyOn(RepoSyncState, "findAllFromSubscription") + .mockResolvedValueOnce(Promise.resolve(reposyncState)); const subscription = { id: 1 }; Subscription.findByPk = jest.fn().mockResolvedValueOnce(subscription); @@ -166,44 +165,35 @@ describe("JiraGetConnectedRepos", () => { expect(findAllFromSubscription).toHaveBeenCalledWith(subscription, { limit: 3, - offset: 0, + offset: 0, where: { [Op.and]: [ { repoName: { - [Op.iLike]: '%github-for-jira%' + [Op.iLike]: "%github-for-jira%" } }, { - [Op.or]:[ + [Op.or]: [ { - branchStatus: 'finished' + branchStatus: "finished" }, { - commitStatus: 'finished' + commitStatus: "finished" }, { - pullStatus: 'finished' + pullStatus: "finished" }, { - buildStatus: 'finished' + buildStatus: "finished" }, { - deploymentStatus: 'finished' + deploymentStatus: "finished" } - ] + ] } ] } }); }); }); - - - - - - - - - From fc21edfb4a2ce8cc0ee84a3fc042ddfd2988d58d Mon Sep 17 00:00:00 2001 From: Kamakshee Samant Date: Mon, 21 Aug 2023 17:15:23 +1000 Subject: [PATCH 13/25] chore: incorporate PR reviews --- static/js/jira-connected-repos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index a9d6b20b19..162c108125 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -1,5 +1,5 @@ $(document).ready(() => { - // to get the focus back on seach bar after comp reload + // to get the focus back on search bar after comp reload $('#repo-search').focus(); const params = new URLSearchParams(window.location.search.substring(1)); const repoSearch = params.get("repoName"); @@ -27,7 +27,7 @@ $(document).ready(() => { $('#repo-search').on('input', function() { // re-render the original list after clearing the search bar if($(this).val().length === 0) { - loadRepos(1) + loadRepos(1); } // search bar is using de-bouncing clearTimeout(repoSearchTimeoutId); From 87b5cce57017e945408d8405aa3ce471dc6adf60 Mon Sep 17 00:00:00 2001 From: Boris Gvozdev Date: Fri, 25 Aug 2023 13:11:54 +1000 Subject: [PATCH 14/25] NONE: outlined TODOs and fixed some dodgy scenarios --- ...d-repos.ts => jira-connected-repos-get.ts} | 25 ++++++++++++------- src/routes/jira/jira-router.ts | 4 +-- static/js/jira-configuration.js | 2 ++ 3 files changed, 20 insertions(+), 11 deletions(-) rename src/routes/jira/{jira-get-connected-repos.ts => jira-connected-repos-get.ts} (86%) diff --git a/src/routes/jira/jira-get-connected-repos.ts b/src/routes/jira/jira-connected-repos-get.ts similarity index 86% rename from src/routes/jira/jira-get-connected-repos.ts rename to src/routes/jira/jira-connected-repos-get.ts index b64f639647..53306a35da 100644 --- a/src/routes/jira/jira-get-connected-repos.ts +++ b/src/routes/jira/jira-connected-repos-get.ts @@ -2,13 +2,20 @@ import { NextFunction, Request, Response } from "express"; import { Op } from "sequelize"; import { RepoSyncState } from "~/src/models/reposyncstate"; import { Subscription, TaskStatus } from "~/src/models/subscription"; +import { escape } from "sequelize/lib/sql-string"; interface Page { pageNum: number; isCurrentPage: boolean; } -export const JiraGetConnectedRepos = async ( +// TODO: add tests: +// - no subscription +// - subscription with different jiraHost +// - sunny path +// - SQL injection test for syncStatusFilter +// - SQL injection test for repoName +export const JiraConnectedReposGet = async ( req: Request, res: Response, next: NextFunction @@ -30,7 +37,7 @@ export const JiraGetConnectedRepos = async ( const subscription = await Subscription.findByPk(subscriptionId); - if (!subscription) { + if (!subscription || subscription.jiraHost !== jiraHost) { req.log.error("Missing Subscription"); res.status(401).send("Missing Subscription"); return; @@ -40,11 +47,11 @@ export const JiraGetConnectedRepos = async ( if (syncStatusFilter && syncStatusFilter !== "all") { syncStatusCondition = { [Op.or]: [ - { branchStatus: `${syncStatusFilter}` }, - { commitStatus: `${syncStatusFilter}` }, - { pullStatus: `${syncStatusFilter}` }, - { buildStatus: `${syncStatusFilter}` }, - { deploymentStatus: `${syncStatusFilter}` } + { branchStatus: `${escape(syncStatusFilter)}` }, + { commitStatus: `${escape(syncStatusFilter)}` }, + { pullStatus: `${escape(syncStatusFilter)}` }, + { buildStatus: `${escape(syncStatusFilter)}` }, + { deploymentStatus: `${escape(syncStatusFilter)}` } ] }; } @@ -54,7 +61,7 @@ export const JiraGetConnectedRepos = async ( [Op.and]: [ { repoName: { - [Op.like]: `%${repoName}%` + [Op.like]: `%${escape(repoName)}%` } }, { @@ -72,7 +79,7 @@ export const JiraGetConnectedRepos = async ( [Op.and]: [ { repoName: { - [Op.like]: `%${repoName}%` + [Op.like]: `%${escape(repoName)}%` } }, { diff --git a/src/routes/jira/jira-router.ts b/src/routes/jira/jira-router.ts index b9b8480e16..5fd1318e07 100644 --- a/src/routes/jira/jira-router.ts +++ b/src/routes/jira/jira-router.ts @@ -10,7 +10,7 @@ import { JiraConnectRouter } from "routes/jira/connect/jira-connect-router"; import { body } from "express-validator"; import { returnOnValidationError } from "routes/api/api-utils"; import { jiraSymmetricJwtMiddleware } from "~/src/middleware/jira-symmetric-jwt-middleware"; -import { JiraGetConnectedRepos } from "~/src/routes/jira/jira-get-connected-repos"; +import { JiraConnectedReposGet } from "routes/jira/jira-connected-repos-get"; import { jiraAdminPermissionsMiddleware } from "middleware/jira-admin-permission-middleware"; import { JiraWorkspacesRouter } from "routes/jira/workspaces/jira-workspaces-router"; import { JiraSecurityWorkspacesRouter } from "routes/jira/security/workspaces/jira-security-workspaces-router"; @@ -38,7 +38,7 @@ JiraRouter.use("/security", jiraSymmetricJwtMiddleware, JiraSecurityWorkspacesRo JiraRouter.get("/", csrfMiddleware, jiraSymmetricJwtMiddleware, jiraAdminPermissionsMiddleware, JiraGet); -JiraRouter.get("/subscription/:subscriptionId/repos", csrfMiddleware, jiraSymmetricJwtMiddleware, JiraGetConnectedRepos); +JiraRouter.get("/subscription/:subscriptionId/repos", csrfMiddleware, jiraSymmetricJwtMiddleware, JiraConnectedReposGet); /******************************************************************************************************************** * TODO: remove this later, keeping this for now cause its out in `Prod` diff --git a/static/js/jira-configuration.js b/static/js/jira-configuration.js index 42196d36e4..83de55785b 100644 --- a/static/js/jira-configuration.js +++ b/static/js/jira-configuration.js @@ -27,6 +27,7 @@ $(".add-organization-link").click(function(event) { }); }); +// TODO: passing JWT in query param is a security risk, we must either populate a session (if not already) or use cookies $(".jiraConfiguration__table__repo_access").click(function (event) { const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); AP.context.getToken(function (token) { @@ -299,6 +300,7 @@ if (genericModalClose != null) { }); } +// TODO: passing JWT in query param is a security risk, we must either populate a session (if not already) or use cookies $(".jiraConfiguration__table__repo_access").click(function (event) { const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); AP.context.getToken(function (token) { From 63854e858d3020b9319ed8112316e34e8c1f3527 Mon Sep 17 00:00:00 2001 From: Boris Gvozdev Date: Mon, 28 Aug 2023 16:21:55 +1000 Subject: [PATCH 15/25] NONE: fix in_progress logic --- src/routes/jira/jira-connected-repos-get.ts | 101 +++++++++++--------- src/routes/jira/jira-get.ts | 3 +- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/routes/jira/jira-connected-repos-get.ts b/src/routes/jira/jira-connected-repos-get.ts index 53306a35da..91bac45352 100644 --- a/src/routes/jira/jira-connected-repos-get.ts +++ b/src/routes/jira/jira-connected-repos-get.ts @@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express"; import { Op } from "sequelize"; import { RepoSyncState } from "~/src/models/reposyncstate"; import { Subscription, TaskStatus } from "~/src/models/subscription"; -import { escape } from "sequelize/lib/sql-string"; +import { sequelize } from "models/sequelize"; interface Page { pageNum: number; @@ -14,6 +14,38 @@ interface Page { // - subscription with different jiraHost // - sunny path // - SQL injection test for syncStatusFilter +const mapFilterSyncStatusToQueryCondition = (filterStatusField: string | undefined) => { + if (!filterStatusField || filterStatusField === "all") { + return {}; + } + if (filterStatusField === "pending") { + return { + [Op.or]: [ + { branchStatus: "pending" }, + { branchStatus: null }, + { commitStatus: "pending" }, + { commitStatus: null }, + { pullStatus: "pending" }, + { pullStatus: null }, + { buildStatus: "pending" }, + { buildStatus: null }, + { deploymentStatus: "pending" }, + { deploymentStatus: null } + ] + }; + } else if (filterStatusField == "failed") { + return { + [Op.or]: [ + { branchStatus: "failed" }, + { commitStatus: "failed" }, + { pullStatus: "failed" }, + { buildStatus: "failed" }, + { deploymentStatus: "failed" } + ] + }; + } + return undefined; +}; // - SQL injection test for repoName export const JiraConnectedReposGet = async ( req: Request, @@ -24,10 +56,10 @@ export const JiraConnectedReposGet = async ( try { const { jiraHost, nonce } = res.locals; const subscriptionId = Number(req.params.subscriptionId); - const page = Number(req.query.page || 1); - const pageSize = Number(req.query.pageSize || 10); - const repoName = req.query.repoName || ""; - const syncStatusFilter = req.query.syncStatus || undefined; + const page = Number(req.query.page) || 1; + const pageSize = Number(req.query.pageSize) || 10; + const filterRepoName = (req.query.repoName || "") as string; + const filterSyncStatus = (req.query.syncStatus || undefined) as (string | undefined); if (!subscriptionId) { req.log.error("Missing Subscription ID"); @@ -43,51 +75,34 @@ export const JiraConnectedReposGet = async ( return; } - let syncStatusCondition = {}; - if (syncStatusFilter && syncStatusFilter !== "all") { - syncStatusCondition = { - [Op.or]: [ - { branchStatus: `${escape(syncStatusFilter)}` }, - { commitStatus: `${escape(syncStatusFilter)}` }, - { pullStatus: `${escape(syncStatusFilter)}` }, - { buildStatus: `${escape(syncStatusFilter)}` }, - { deploymentStatus: `${escape(syncStatusFilter)}` } - ] - }; + const syncStatusCondition = mapFilterSyncStatusToQueryCondition(filterSyncStatus); + if (syncStatusCondition === undefined) { + req.log.error({ filterStatusField: filterSyncStatus }, "invalid status field"); + res.status(400).send("invalid status field"); + return; } - const reposCount = await RepoSyncState.countSubscriptionRepos(subscription, { - where: { - [Op.and]: [ - { - repoName: { - [Op.like]: `%${escape(repoName)}%` - } - }, - { - ...syncStatusCondition - } - ] - + const repoFilterCondition = { + repoName: { + [Op.like]: sequelize.literal(sequelize.escape(`%${filterRepoName}%`)) } + }; + + const filterCondition = { + [Op.and]: [ + repoFilterCondition, + syncStatusCondition + ] + }; + + const reposCount = await RepoSyncState.countSubscriptionRepos(subscription, { + where: filterCondition }); const offset = page == 1 ? 0 : (page - 1) * pageSize; const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription, { - where: { - [Op.and]: [ - { - repoName: { - [Op.like]: `%${escape(repoName)}%` - } - }, - { - ...syncStatusCondition - } - ] - - }, + where: filterCondition, limit: pageSize, offset }); @@ -189,6 +204,6 @@ const mapTaskStatus = (syncStatus: TaskStatus): string => { case "failed": return "FAILED"; default: - return syncStatus; + return "IN PROGRESS"; } }; diff --git a/src/routes/jira/jira-get.ts b/src/routes/jira/jira-get.ts index d935638edc..0c815039b0 100644 --- a/src/routes/jira/jira-get.ts +++ b/src/routes/jira/jira-get.ts @@ -175,7 +175,8 @@ const renderJiraCloudAndEnterpriseServer = async (res: Response, req: Request): hasConnections, useNewSPAExperience, APP_URL: process.env.APP_URL, - enableRepoConnectedPage: await booleanFlag(BooleanFlags.ENABLE_CONNECTED_REPOS_VIEW, jiraHost), + // TODO: uncomment me + enableRepoConnectedPage: true, //await booleanFlag(BooleanFlags.ENABLE_CONNECTED_REPOS_VIEW, jiraHost), csrfToken: req.csrfToken(), nonce }); From 17e8ec203d344365874806e37892ef25a52b0ad5 Mon Sep 17 00:00:00 2001 From: Boris Gvozdev Date: Mon, 28 Aug 2023 18:00:08 +1000 Subject: [PATCH 16/25] ARC-968: fix navigation --- .../jira-atlassian-connect-get.ts | 8 +++++++ src/routes/jira/jira-connected-repos-get.ts | 12 ++++++++--- static/js/jira-configuration.js | 21 ++++++++++++------- static/js/jira-connected-repos.js | 12 +++++++---- static/js/navigation.js | 13 ++++++++++++ views/jira-connected-repos.hbs | 4 ++-- 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/routes/jira/atlassian-connect/jira-atlassian-connect-get.ts b/src/routes/jira/atlassian-connect/jira-atlassian-connect-get.ts index 1997e66dd0..db181caa35 100644 --- a/src/routes/jira/atlassian-connect/jira-atlassian-connect-get.ts +++ b/src/routes/jira/atlassian-connect/jira-atlassian-connect-get.ts @@ -219,6 +219,14 @@ const modules = { url: "/spa", location: "none", conditions: adminCondition + }, { + url: "/jira/subscription/{ac.subscriptionId}/repos?pageNumber={ac.pageNumber}&repoName={ac.repoName}&syncStatus={ac.syncStatus}", + name: { + value: "Sync status" + }, + conditions: adminCondition, + key: "gh-addon-subscription-repos", + location: "none" } ], webSections: [ diff --git a/src/routes/jira/jira-connected-repos-get.ts b/src/routes/jira/jira-connected-repos-get.ts index 91bac45352..9b810ece6a 100644 --- a/src/routes/jira/jira-connected-repos-get.ts +++ b/src/routes/jira/jira-connected-repos-get.ts @@ -56,7 +56,7 @@ export const JiraConnectedReposGet = async ( try { const { jiraHost, nonce } = res.locals; const subscriptionId = Number(req.params.subscriptionId); - const page = Number(req.query.page) || 1; + const pageNumber = Number(req.query.pageNumber) || 1; const pageSize = Number(req.query.pageSize) || 10; const filterRepoName = (req.query.repoName || "") as string; const filterSyncStatus = (req.query.syncStatus || undefined) as (string | undefined); @@ -67,6 +67,12 @@ export const JiraConnectedReposGet = async ( return; } + if (pageSize > 100) { + req.log.error("pageSize cannot be larger than 100"); + res.status(400).send("pageSize cannot be larger than 100"); + return; + } + const subscription = await Subscription.findByPk(subscriptionId); if (!subscription || subscription.jiraHost !== jiraHost) { @@ -99,7 +105,7 @@ export const JiraConnectedReposGet = async ( where: filterCondition }); - const offset = page == 1 ? 0 : (page - 1) * pageSize; + const offset = pageNumber == 1 ? 0 : (pageNumber - 1) * pageSize; const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription, { where: filterCondition, @@ -125,7 +131,7 @@ export const JiraConnectedReposGet = async ( subscriptionId, csrfToken: req.csrfToken(), nonce, - ...getPaginationState(page, pageSize, reposCount) + ...getPaginationState(pageNumber, pageSize, reposCount) }); } catch (error) { diff --git a/static/js/jira-configuration.js b/static/js/jira-configuration.js index 83de55785b..f42c7ef1a9 100644 --- a/static/js/jira-configuration.js +++ b/static/js/jira-configuration.js @@ -30,9 +30,13 @@ $(".add-organization-link").click(function(event) { // TODO: passing JWT in query param is a security risk, we must either populate a session (if not already) or use cookies $(".jiraConfiguration__table__repo_access").click(function (event) { const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); - AP.context.getToken(function (token) { - window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}`; - }); + AP.navigator.go( + 'addonmodule', + { + moduleKey: "gh-addon-subscription-repos", + customData: { subscriptionId } + } + ); }); $(".add-enterprise-link").click(function(event) { @@ -300,12 +304,15 @@ if (genericModalClose != null) { }); } -// TODO: passing JWT in query param is a security risk, we must either populate a session (if not already) or use cookies $(".jiraConfiguration__table__repo_access").click(function (event) { const subscriptionId = $(event.target.parentElement).attr('data-subscription-id'); - AP.context.getToken(function (token) { - window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}`; - }); + AP.navigator.go( + 'addonmodule', + { + moduleKey: "gh-addon-subscription-repos", + customData: { subscriptionId } + } + ); }); // When the user clicks anywhere outside of the modal, close it diff --git a/static/js/jira-connected-repos.js b/static/js/jira-connected-repos.js index 82fb10cd26..1ff66b85a5 100644 --- a/static/js/jira-connected-repos.js +++ b/static/js/jira-connected-repos.js @@ -26,9 +26,13 @@ $(document).ready(() => { const syncStatus =$("#status-filter").val(); const repoName = $("#repo-search").val(); const subscriptionId = $(".jiraConnectedRepos_pagination").attr('data-subscription-id'); - AP.context.getToken(function (token) { - window.location.href = `/jira/subscription/${subscriptionId}/repos?jwt=${token}&page=${pageNumber}&repoName=${repoName}&syncStatus=${syncStatus}`; - }); + AP.navigator.go( + 'addonmodule', + { + moduleKey: "gh-addon-subscription-repos", + customData: { subscriptionId, pageNumber, repoName, syncStatus } + } + ); }; $(".jiraConnectedRepos__table__cell__repoName").click((event) => { @@ -90,7 +94,7 @@ const getStatusIconInfo = (status) => { const mapErrorToMessage = (errorCode) => { if(errorCode == "CONNECTION_ERROR") { return "This is caused by connection error. To fix this, restarting backfilling may rectify this error. If this error persists, please contact the Atlassian support team." - } + } return errorCode; }; diff --git a/static/js/navigation.js b/static/js/navigation.js index b491759798..49158062d4 100644 --- a/static/js/navigation.js +++ b/static/js/navigation.js @@ -8,3 +8,16 @@ $(".go-back").click(function (event) { history.back(); } }); + +$(".go-main-admin").click(function (event) { + event.preventDefault(); + + AP.navigator.go( + 'addonmodule', + { + moduleKey: "gh-addon-admin", + } + ); +}); + + diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index f3a95c4a65..5f6f181743 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -20,7 +20,7 @@
-
@@ -167,4 +167,4 @@ - \ No newline at end of file + From a6c1ad1cd9b5b2111f619966c5d87ae76e397433 Mon Sep 17 00:00:00 2001 From: Boris Gvozdev Date: Tue, 29 Aug 2023 14:19:24 +1000 Subject: [PATCH 17/25] ARC-968: fix layout --- static/css/jira-connected-repos.css | 8 ++++---- views/jira-connected-repos.hbs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/static/css/jira-connected-repos.css b/static/css/jira-connected-repos.css index 4bec4fa5cf..4aaff1b9c0 100644 --- a/static/css/jira-connected-repos.css +++ b/static/css/jira-connected-repos.css @@ -1,6 +1,5 @@ .jiraConnectedRepos { box-sizing: border-box; - height: 100vh; overflow-y: hidden; padding: 3.5em 4.5em; } @@ -16,7 +15,8 @@ } .jiraConnectedRepos_content { - margin-top: 2em; + margin: 0 auto; + max-width: 560px; } .jiraConnectedRepos_content > .jiraConnectedRepos__tableContainer { @@ -44,7 +44,7 @@ } .jiraConnectedRepos__table__head__title:not(:last-child) { - width: 30%; + } .jiraConnectedRepos__table__body @@ -130,7 +130,7 @@ } .jiraConnectedRepos_pagination .prev-page { - margin-right: 10px; + margin-right: 10px; } .jiraConnectedRepos_pagination .next-page span, diff --git a/views/jira-connected-repos.hbs b/views/jira-connected-repos.hbs index 5f6f181743..bdce4bef85 100644 --- a/views/jira-connected-repos.hbs +++ b/views/jira-connected-repos.hbs @@ -24,14 +24,14 @@ Go back
+
+ +

GitHub connected repositories

- - -