From de91e0ddefb56b1c4793743dbb4e4f85c5a370b7 Mon Sep 17 00:00:00 2001 From: Ritik Kapoor Date: Tue, 17 Dec 2024 08:27:43 +0000 Subject: [PATCH] feat: [CODE-2765]: add Branch rules support on Account, Org, and Project level (#3143) * fix: [CODE-2765] prettier * fix: [CODE-2765] fix lint * feat: [CODE-1509] added BranchProtectionHeader with permission utils * feat: [CODE-1509] added BranchProtectionForm with permissions props * feat: [CODE-1509] added BranchProtectionHeader with permission util abstracted * feat: [CODE-1509] added BranchProtectionListing with permission props * feat: [CODE-1509] added BranchProtectionHeader with permission props * fix: [CODE-1509] lint * feat: [CODE-1509] permission utility * feat: [CODE-2765]: add Branch rules support on Account, Org, and Project level --- web/config/moduleFederation.config.js | 2 +- web/src/RouteDefinitions.ts | 19 +- web/src/RouteDestinations.tsx | 36 ++- .../BranchProtectionForm.tsx | 86 +++++-- .../BranchProtectionHeader.module.scss | 7 + .../BranchProtectionHeader.module.scss.d.ts | 1 + .../BranchProtectionHeader.tsx | 70 ++++-- .../BranchProtectionListing.tsx | 227 ++++++++++++------ web/src/framework/strings/stringTypes.ts | 2 + web/src/i18n/strings.en.yaml | 4 +- .../Labels/LabelsHeader/LabelsHeader.tsx | 2 +- .../ManageLabels/ManageLabels.module.scss | 22 -- .../ManageSpace/ManageLabels/ManageLabels.tsx | 44 ---- .../ManageRepositories.module.scss | 98 ++++++++ .../ManageRepositories.module.scss.d.ts} | 2 + .../ManageRepositories/ManageRepositories.tsx | 80 ++++++ .../RepositorySettings/RepositorySettings.tsx | 29 ++- web/src/pages/SpaceSettings/SpaceSettings.tsx | 6 + web/src/services/code/index.tsx | 1 + web/src/utils/Utils.ts | 111 +++++++++ 20 files changed, 638 insertions(+), 211 deletions(-) delete mode 100644 web/src/pages/ManageSpace/ManageLabels/ManageLabels.module.scss delete mode 100644 web/src/pages/ManageSpace/ManageLabels/ManageLabels.tsx create mode 100644 web/src/pages/ManageSpace/ManageRepositories/ManageRepositories.module.scss rename web/src/pages/ManageSpace/{ManageLabels/ManageLabels.module.scss.d.ts => ManageRepositories/ManageRepositories.module.scss.d.ts} (89%) create mode 100644 web/src/pages/ManageSpace/ManageRepositories/ManageRepositories.tsx diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index 78016e676..c2606c2fe 100644 --- a/web/config/moduleFederation.config.js +++ b/web/config/moduleFederation.config.js @@ -52,7 +52,7 @@ module.exports = { './Webhooks': './src/pages/Webhooks/Webhooks.tsx', './WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx', './Search': './src/pages/Search/CodeSearchPage.tsx', - './Labels': './src/pages/ManageSpace/ManageLabels/ManageLabels.tsx', + './Labels': './src/pages/ManageSpace/ManageRepositories/ManageRepositories.tsx', './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx', './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx', './HAREnterpriseApp': './src/ar/app/EnterpriseApp.tsx', diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index a12f4f30d..474da0ada 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -71,7 +71,12 @@ export interface CODERoutes extends CDERoutes, ARRoutes { toCODEHome: () => string toCODESpaceAccessControl: (args: Required>) => string - toCODESpaceSettings: (args: RequiredField, 'space'>) => string + toCODESpaceSettings: ( + args: RequiredField, 'space'> + ) => string + toCODEManageRepositories: ( + args: RequiredField, 'space'> + ) => string toCODEPipelines: (args: Required>) => string toCODEPipelineEdit: (args: Required>) => string toCODEPipelineSettings: (args: Required>) => string @@ -104,7 +109,6 @@ export interface CODERoutes extends CDERoutes, ARRoutes { args: RequiredField, 'repoPath'> ) => string toCODESpaceSearch: (args: Required>) => string - toCODESpaceLabels: (args: Required>) => string toCODERepositorySearch: (args: Required>) => string toCODESemanticSearch: (args: Required>) => string toCODEExecutions: (args: Required>) => string @@ -128,8 +132,14 @@ export const routes: CODERoutes = { toCODEHome: () => `/`, toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`, - toCODESpaceSettings: ({ space, settingSection }) => - `/settings/${space}/project${settingSection ? '/' + settingSection : ''}`, + toCODESpaceSettings: ({ space, settingSection, ruleId, settingSectionMode }) => + `/settings/${space}/project${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${ + settingSectionMode ? '/' + settingSectionMode : '' + }`, + toCODEManageRepositories: ({ space, settingSection, ruleId, settingSectionMode }) => + `/${space}/manage-repositories${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${ + settingSectionMode ? '/' + settingSectionMode : '' + }`, toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`, toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`, toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`, @@ -160,7 +170,6 @@ export const routes: CODERoutes = { toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`, toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`, toCODETags: ({ repoPath }) => `/${repoPath}/tags`, - toCODESpaceLabels: ({ space }) => `/${space}/labels`, toCODESettings: ({ repoPath, settingSection, ruleId, settingSectionMode }) => `/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${ settingSectionMode ? '/' + settingSectionMode : '' diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index e7da86c6d..4d0f80880 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -55,7 +55,7 @@ import PipelineSettings from 'components/PipelineSettings/PipelineSettings' import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails' import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing' import GitspaceCreate from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate' -import ManageLabels from 'pages/ManageSpace/ManageLabels/ManageLabels' +import ManageRepositories from 'pages/ManageSpace/ManageRepositories/ManageRepositories' const ArApp = lazy(() => import('@ar/gitness/ArApp')) @@ -87,6 +87,17 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations - - - + + + diff --git a/web/src/components/BranchProtection/BranchProtectionForm/BranchProtectionForm.tsx b/web/src/components/BranchProtection/BranchProtectionForm/BranchProtectionForm.tsx index 7b163f8b6..3a8f1e33f 100644 --- a/web/src/components/BranchProtection/BranchProtectionForm/BranchProtectionForm.tsx +++ b/web/src/components/BranchProtection/BranchProtectionForm/BranchProtectionForm.tsx @@ -38,9 +38,12 @@ import { useGet, useMutate } from 'restful-react' import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils' import { useStrings } from 'framework/strings' import { + LabelsPageScope, REGEX_VALID_REPO_NAME, RulesFormPayload, + getEditPermissionRequestFromScope, getErrorMessage, + getScopeData, permissionProps, rulesFormInitialPayload } from 'utils/Utils' @@ -55,6 +58,7 @@ import type { import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useAppContext } from 'AppContext' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { getConfig } from 'services/config' import ProtectionRulesForm from './ProtectionRulesForm/ProtectionRulesForm' import Include from '../../../icons/Include.svg?url' import Exclude from '../../../icons/Exclude.svg?url' @@ -62,33 +66,50 @@ import BypassList from './BypassList' import css from './BranchProtectionForm.module.scss' const BranchProtectionForm = (props: { - ruleUid: string + currentRule?: OpenapiRule editMode: boolean repoMetadata?: RepoRepositoryOutput | undefined refetchRules: () => void settingSectionMode: SettingTypeMode + currentPageScope: LabelsPageScope }) => { const { routes, routingId, standalone, hooks } = useAppContext() const { ruleId } = useGetRepositoryMetadata() const { showError, showSuccess } = useToaster() - const { editMode = false, repoMetadata, ruleUid, refetchRules, settingSectionMode } = props + const space = useGetSpaceParam() + const { editMode = false, repoMetadata, currentRule, refetchRules, settingSectionMode, currentPageScope } = props const { getString } = useStrings() - const { data: rule } = useGet({ - path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${ruleId}`, - lazy: !repoMetadata && !ruleId - }) const [searchTerm, setSearchTerm] = useState('') const [searchStatusTerm, setSearchStatusTerm] = useState('') + const { scopeRef } = currentRule?.scope ? getScopeData(space, currentRule?.scope, standalone) : { scopeRef: space } + + const getUpdateRulePath = () => + currentRule?.scope === 0 && repoMetadata + ? `/repos/${repoMetadata?.path}/+/rules/${encodeURIComponent(ruleId)}` + : `/spaces/${scopeRef}/+/rules/${encodeURIComponent(ruleId)}` + + const getCreateRulePath = () => + currentPageScope === LabelsPageScope.REPOSITORY + ? `/repos/${repoMetadata?.path}/+/rules` + : `/spaces/${space}/+/rules` + + const { data: rule } = useGet({ + base: getConfig('code/api/v1'), + path: getUpdateRulePath(), + lazy: !ruleId + }) const { mutate } = useMutate({ verb: 'POST', - path: `/api/v1/repos/${repoMetadata?.path}/+/rules/` + base: getConfig('code/api/v1'), + path: getCreateRulePath() }) const { mutate: updateRule } = useMutate({ verb: 'PATCH', - path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${ruleId}` + base: getConfig('code/api/v1'), + path: getUpdateRulePath() }) const { data: users } = useGet({ path: `/api/v1/principals`, @@ -111,10 +132,19 @@ const BranchProtectionForm = (props: { const usersArrayCurr = transformUserArray?.map(user => `${user.id} ${user.display_name}`) const [userArrayState, setUserArrayState] = useState(usersArrayCurr) + const getUpdateChecksPath = () => + currentRule?.scope === 0 && repoMetadata + ? `/repos/${repoMetadata?.path}/+/checks/recent` + : `/spaces/${scopeRef}/+/checks/recent` + const { data: statuses } = useGet({ - path: `/api/v1/repos/${repoMetadata?.path}/+/checks/recent`, + base: getConfig('code/api/v1'), + path: getUpdateChecksPath(), queryParams: { - query: searchStatusTerm + query: searchStatusTerm, + ...(!repoMetadata && { + recursive: true + }) }, debounce: 500 }) @@ -141,10 +171,20 @@ const BranchProtectionForm = (props: { showSuccess(successMessage) resetForm() history.push( - routes.toCODESettings({ - repoPath: repoMetadata?.path as string, - settingSection: SettingsTab.branchProtection - }) + repoMetadata + ? routes.toCODESettings({ + repoPath: repoMetadata?.path as string, + settingSection: SettingsTab.branchProtection + }) + : standalone + ? routes.toCODESpaceSettings({ + space, + settingSection: SettingsTab.branchProtection + }) + : routes.toCODEManageRepositories({ + space, + settingSection: SettingsTab.branchProtection + }) ) refetchRules?.() } catch (exception) { @@ -223,17 +263,11 @@ const BranchProtectionForm = (props: { } return rulesFormInitialPayload // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editMode, rule, ruleUid, users]) - const space = useGetSpaceParam() - const permPushResult = hooks?.usePermissionTranslate?.( - { - resource: { - resourceType: 'CODE_REPOSITORY', - resourceIdentifier: repoMetadata?.identifier as string - }, - permissions: ['code_repo_edit'] - }, - [space] + }, [editMode, rule, currentRule, users]) + + const permPushResult = hooks?.usePermissionTranslate( + getEditPermissionRequestFromScope(space, currentRule?.scope ?? 0, repoMetadata), + [space, currentRule?.scope, repoMetadata] ) return ( @@ -392,7 +426,7 @@ const BranchProtectionForm = (props: { flex={{ align: 'center-center' }} padding={{ top: 'xxlarge', left: 'small' }}> diff --git a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss index ad81f51dd..891fb6ea6 100644 --- a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss +++ b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss @@ -59,3 +59,10 @@ .cancelButton { margin-left: var(--spacing-small) !important; } + +.scopeCheckbox { + display: flex; + align-items: center; + padding-right: var(--spacing-3) !important; + padding-left: var(--spacing-3) !important; +} diff --git a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss.d.ts b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss.d.ts index 1063dd5a1..59ac78869 100644 --- a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss.d.ts +++ b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.module.scss.d.ts @@ -20,6 +20,7 @@ export declare const cancelButton: string export declare const main: string export declare const noData: string export declare const row: string +export declare const scopeCheckbox: string export declare const table: string export declare const title: string export declare const toggle: string diff --git a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.tsx b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.tsx index e00cf76f4..7ade2f039 100644 --- a/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.tsx +++ b/web/src/components/BranchProtection/BranchProtectionHeader/BranchProtectionHeader.tsx @@ -15,19 +15,23 @@ */ import { useHistory } from 'react-router-dom' import React, { useState } from 'react' -import { Container, Layout, FlexExpander, ButtonVariation, Button } from '@harnessio/uicore' +import { Container, Layout, FlexExpander, ButtonVariation, Button, Checkbox } from '@harnessio/uicore' +import { Render } from 'react-jsx-match' import { useStrings } from 'framework/strings' import { CodeIcon, GitInfoProps, SettingTypeMode } from 'utils/GitUtils' import { useAppContext } from 'AppContext' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' -import { permissionProps } from 'utils/Utils' +import { getEditPermissionRequestFromIdentifier, permissionProps } from 'utils/Utils' import css from './BranchProtectionHeader.module.scss' const BranchProtectionHeader = ({ repoMetadata, loading, onSearchTermChanged, - activeTab + activeTab, + showParentScopeFilter, + inheritRules, + setInheritRules }: BranchProtectionHeaderProps) => { const history = useHistory() const [searchTerm, setSearchTerm] = useState('') @@ -37,17 +41,10 @@ const BranchProtectionHeader = ({ const space = useGetSpaceParam() - const permPushResult = hooks?.usePermissionTranslate?.( - { - resource: { - resourceType: 'CODE_REPOSITORY', - resourceIdentifier: repoMetadata?.identifier as string - }, - permissions: ['code_repo_edit'] - }, - [space] - ) - + const permPushResult = hooks?.usePermissionTranslate(getEditPermissionRequestFromIdentifier(space, repoMetadata), [ + space, + repoMetadata + ]) return ( @@ -56,16 +53,42 @@ const BranchProtectionHeader = ({ text={getString('branchProtection.newRule')} icon={CodeIcon.Add} onClick={() => - history.push( - routes.toCODESettings({ - repoPath: repoMetadata?.path as string, - settingSection: activeTab, - settingSectionMode: SettingTypeMode.NEW - }) - ) + repoMetadata + ? history.push( + routes.toCODESettings({ + repoPath: repoMetadata?.path as string, + settingSection: activeTab, + settingSectionMode: SettingTypeMode.NEW + }) + ) + : standalone + ? history.push( + routes.toCODESpaceSettings({ + space, + settingSection: activeTab, + settingSectionMode: SettingTypeMode.NEW + }) + ) + : history.push( + routes.toCODEManageRepositories({ + space, + settingSection: activeTab, + settingSectionMode: SettingTypeMode.NEW + }) + ) } {...permissionProps(permPushResult, standalone)} /> + + { + setInheritRules(event.currentTarget.checked) + }} + /> + { +interface BranchProtectionHeaderProps extends Partial> { loading?: boolean activeTab?: string + showParentScopeFilter: boolean + inheritRules: boolean + setInheritRules: (value: boolean) => void onSearchTermChanged: (searchTerm: string) => void } diff --git a/web/src/components/BranchProtection/BranchProtectionListing.tsx b/web/src/components/BranchProtection/BranchProtectionListing.tsx index e00f16096..f3e250a01 100644 --- a/web/src/components/BranchProtection/BranchProtectionListing.tsx +++ b/web/src/components/BranchProtection/BranchProtectionListing.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Container, Layout, @@ -35,8 +35,8 @@ import type { CellProps, Column } from 'react-table' import { useGet, useMutate } from 'restful-react' import { FontVariation } from '@harnessio/design-system' import { Position } from '@blueprintjs/core' -import { useHistory } from 'react-router-dom' -import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' +import { useHistory, useParams } from 'react-router-dom' +import { Icon } from '@harnessio/icons' import { useQueryParams } from 'hooks/useQueryParams' import { usePageIndex } from 'hooks/usePageIndex' import { @@ -47,7 +47,12 @@ import { Rule, RuleFields, BranchProtectionRulesMapType, - createRuleFieldsMap + createRuleFieldsMap, + LabelsPageScope, + getScopeData, + getScopeIcon, + getEditPermissionRequestFromScope, + getEditPermissionRequestFromIdentifier } from 'utils/Utils' import { SettingTypeMode } from 'utils/GitUtils' import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' @@ -56,46 +61,70 @@ import { useStrings } from 'framework/strings' import { useConfirmAct } from 'hooks/useConfirmAction' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' -import type { OpenapiRule, ProtectionPattern } from 'services/code' +import type { OpenapiRule, ProtectionPattern, RepoRepositoryOutput } from 'services/code' import { useAppContext } from 'AppContext' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' +import type { CODEProps } from 'RouteDefinitions' +import { getConfig } from 'services/config' import Include from '../../icons/Include.svg?url' import Exclude from '../../icons/Exclude.svg?url' import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm' import BranchProtectionHeader from './BranchProtectionHeader/BranchProtectionHeader' import css from './BranchProtectionListing.module.scss' -const BranchProtectionListing = (props: { activeTab: string }) => { - const { activeTab } = props +const BranchProtectionListing = (props: { + activeTab: string + repoMetadata?: RepoRepositoryOutput + currentPageScope: LabelsPageScope +}) => { + const { activeTab, repoMetadata, currentPageScope } = props const { getString } = useStrings() const { showError, showSuccess } = useToaster() const history = useHistory() - const { routes } = useAppContext() + const { routes, standalone, hooks } = useAppContext() const pageBrowser = useQueryParams() const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 const [page, setPage] = usePageIndex(pageInit) const [searchTerm, setSearchTerm] = useState('') - const [curRuleName, setCurRuleName] = useState('') - const { repoMetadata, settingSection, ruleId, settingSectionMode } = useGetRepositoryMetadata() + const [currentRule, setCurrentRule] = useState() + const { settingSection, ruleId, settingSectionMode } = useParams() const newRule = settingSection && settingSectionMode === SettingTypeMode.NEW const editRule = settingSection !== '' && ruleId !== '' && settingSectionMode === SettingTypeMode.EDIT + const [showParentScopeFilter, setShowParentScopeFilter] = useState(true) + const [inheritRules, setInheritRules] = useState(false) + const space = useGetSpaceParam() + + useEffect(() => { + if (currentPageScope) { + if (currentPageScope === LabelsPageScope.ACCOUNT) setShowParentScopeFilter(false) + else if (currentPageScope === LabelsPageScope.SPACE) setShowParentScopeFilter(false) + } + }, [currentPageScope, standalone]) + + const getRulesPath = () => + currentPageScope === LabelsPageScope.REPOSITORY + ? `/repos/${repoMetadata?.path}/+/rules` + : `/spaces/${space}/+/rules` + const { data: rules, refetch: refetchRules, loading: loadingRules, response } = useGet({ - path: `/api/v1/repos/${repoMetadata?.path}/+/rules`, + base: getConfig('code/api/v1'), + path: getRulesPath(), queryParams: { limit: LIST_FETCHING_LIMIT, + inherited: inheritRules, page, sort: 'date', order: 'desc', query: searchTerm }, debounce: 500, - lazy: !repoMetadata || !!editRule + lazy: !!editRule }) const branchProtectionRules = { @@ -187,24 +216,83 @@ const BranchProtectionListing = (props: { activeTab: string }) => { } } + function navigateToSettings({ + repoMetadata: metaData, + standalone: isStandalone, + space: currentSpace, + scope: currentScope, + settingSection: section, + settingSectionMode: sectionMode, + ruleId: id + }: { + repoMetadata?: RepoRepositoryOutput + standalone?: boolean + space: string + scope?: number + settingSection?: string + settingSectionMode?: string + ruleId?: string + }) { + const { scopeRef } = currentScope + ? getScopeData(currentSpace, currentScope, isStandalone ?? false) + : { scopeRef: currentSpace } + + if (metaData && currentScope === 0) { + history.push( + routes.toCODESettings({ + repoPath: metaData.path as string, + settingSection: section, + settingSectionMode: sectionMode, + ruleId: id + }) + ) + } else if (isStandalone) { + history.push( + routes.toCODESpaceSettings({ + space: currentSpace, + settingSection: section, + settingSectionMode: sectionMode, + ruleId: id + }) + ) + } else { + history.push( + routes.toCODEManageRepositories({ + space: scopeRef, + settingSection: section, + settingSectionMode: sectionMode, + ruleId: id + }) + ) + } + } + const columns: Column[] = useMemo( () => [ { id: 'title', width: '100%', Cell: ({ row }: CellProps) => { + const { scopeRef } = getScopeData(space, row.original?.scope ?? 1, standalone) + const getRuleIDPath = () => + row.original?.scope === 0 && repoMetadata + ? `/repos/${repoMetadata?.path}/+/rules/${encodeURIComponent(row.original?.identifier as string)}` + : `/spaces/${scopeRef}/+/rules/${encodeURIComponent(row.original?.identifier as string)}` + const [checked, setChecked] = useState( row.original.state === 'active' || row.original.state === 'monitor' ? true : false ) - const { mutate } = useMutate({ + const { mutate: toggleRule } = useMutate({ verb: 'PATCH', - path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${row.original?.identifier}` + base: getConfig('code/api/v1'), + path: getRuleIDPath() }) const [popoverDialogOpen, setPopoverDialogOpen] = useState(false) const { mutate: deleteRule } = useMutate({ verb: 'DELETE', - path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${row.original.identifier}` + base: getConfig('code/api/v1'), + path: getRuleIDPath() }) const confirmDelete = useConfirmAct() const includeElements = (row.original?.pattern as ProtectionPattern)?.include?.map( @@ -266,20 +354,13 @@ const BranchProtectionListing = (props: { activeTab: string }) => { const nonEmptyRules = checkAppliedRules(row.original.definition as Rule, branchProtectionRules) - const { hooks, standalone } = useAppContext() - - const space = useGetSpaceParam() - - const permPushResult = hooks?.usePermissionTranslate?.( - { - resource: { - resourceType: 'CODE_REPOSITORY', - resourceIdentifier: repoMetadata?.identifier as string - }, - permissions: ['code_repo_edit'] - }, - [space] + const scope = row.original?.scope + const permPushResult = hooks?.usePermissionTranslate( + getEditPermissionRequestFromScope(space, scope ?? 0, repoMetadata), + [space, repoMetadata, scope] ) + + const scopeIcon = getScopeIcon(row.original?.scope, standalone) return ( @@ -313,7 +394,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => { text={getString('confirm')} onClick={() => { const data = { state: checked ? 'disabled' : 'active' } - mutate(data) + toggleRule(data) .then(() => { showSuccess(getString('branchProtection.ruleUpdated')) }) @@ -348,10 +429,12 @@ const BranchProtectionListing = (props: { activeTab: string }) => { - - {row.original.identifier} - - + + {scopeIcon && } + {row.original.identifier} + {!!row.original.description && ( { iconName: 'Edit', text: getString('branchProtection.editRule'), onClick: () => { - history.push( - routes.toCODESettings({ - repoPath: repoMetadata?.path as string, - settingSection: settingSection, - settingSectionMode: SettingTypeMode.EDIT, - ruleId: String(row.original.identifier) - }) - ) + setCurrentRule(row.original) + navigateToSettings({ + repoMetadata, + standalone, + space, + scope, + settingSection, + settingSectionMode: SettingTypeMode.EDIT, + ruleId: String(row.original.identifier) + }) } }, { @@ -457,40 +542,35 @@ const BranchProtectionListing = (props: { activeTab: string }) => { ], // eslint-disable-next-line react-hooks/exhaustive-deps [history, getString, repoMetadata?.path, setPage, showError, showSuccess] ) - const { hooks, standalone } = useAppContext() - const space = useGetSpaceParam() - - const permPushResult = hooks?.usePermissionTranslate?.( - { - resource: { - resourceType: 'CODE_REPOSITORY', - resourceIdentifier: repoMetadata?.identifier as string - }, - permissions: ['code_repo_edit'] - }, - [space] - ) + const permPushResult = hooks?.usePermissionTranslate(getEditPermissionRequestFromIdentifier(space, repoMetadata), [ + space, + repoMetadata + ]) return ( - {repoMetadata && !newRule && !editRule && ( + {!newRule && !editRule && ( ) => { setSearchTerm(value) setPage(1) }} - repoMetadata={repoMetadata} + inheritRules={inheritRules} + setInheritRules={setInheritRules} + {...(repoMetadata && { repoMetadata: repoMetadata })} /> )} {newRule || editRule ? ( ) : ( @@ -503,15 +583,16 @@ const BranchProtectionListing = (props: { activeTab: string }) => { data={rules} getRowClassName={() => css.row} onRowClick={row => { - setCurRuleName(row.identifier as string) - history.push( - routes.toCODESettings({ - repoPath: repoMetadata?.path as string, - settingSection: settingSection, - settingSectionMode: SettingTypeMode.EDIT, - ruleId: String(row.identifier) - }) - ) + setCurrentRule(row) + navigateToSettings({ + repoMetadata, + standalone, + space, + scope: row.scope, + settingSection, + settingSectionMode: SettingTypeMode.EDIT, + ruleId: String(row.identifier) + }) }} /> @@ -525,13 +606,13 @@ const BranchProtectionListing = (props: { activeTab: string }) => { message={getString('branchProtection.ruleEmpty')} buttonText={getString('branchProtection.newRule')} onButtonClick={() => { - history.push( - routes.toCODESettings({ - repoPath: repoMetadata?.path as string, - settingSection: activeTab, - settingSectionMode: SettingTypeMode.NEW - }) - ) + navigateToSettings({ + repoMetadata, + standalone, + space, + settingSection, + settingSectionMode: SettingTypeMode.NEW + }) }} permissionProp={permissionProps(permPushResult, standalone)} /> diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 78f562de3..1622b2b66 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -129,6 +129,7 @@ export interface StringsMap { 'branchProtection.ruleEmpty': string 'branchProtection.ruleUpdated': string 'branchProtection.saveRule': string + 'branchProtection.showRulesScope': string 'branchProtection.statusCheck': string 'branchProtection.targetBranches': string 'branchProtection.targetPatternHint': string @@ -592,6 +593,7 @@ export interface StringsMap { makeRequired: string manageApiToken: string manageCredText: string + manageRepositories: string manageRepository: string markAsDraft: string matchPassword: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index bbaf1534d..8eb6d652d 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -31,6 +31,7 @@ commitChanges: Commit changes pullRequests: Pull Requests settings: Settings manageRepository: Manage Repository +manageRepositories: Manage Repositories newFile: New File editFile: Edit File prev: Prev @@ -990,7 +991,7 @@ branchProtection: targetPatternHint: Match branches using globstar patterns (e.g. "golden", "feature-*", "releases/**") defaultBranch: Default branch bypassList: Bypass List - newRule: New branch rule + newRule: New Branch Rule allRepoOwners: Allow users with edit permission on the repository to bypass protectionSelectAll: 'Rules: Select all that apply' requireMinReviewersTitle: Require a minimum number of reviewers @@ -1025,6 +1026,7 @@ branchProtection: deleteRule: Delete Rule ruleDeleted: Rule Deleted ruleEmpty: There are no rules in your repo. Click the button below to create a rule. + showRulesScope: Show rules from parent scopes createRule: Create rule deleteProtectionRule: Delete protection rule deleteText: "Are you sure to delete the rule, '{{rule}}'?" diff --git a/web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx b/web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx index 30e9de796..e0a97a2ce 100644 --- a/web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx +++ b/web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx @@ -37,7 +37,7 @@ const LabelsHeader = ({ //ToDo: check space permissions as well in case of spaces return ( - +