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
This commit is contained in:
Ritik Kapoor 2024-12-17 08:27:43 +00:00 committed by Harness
parent fa675f322c
commit de91e0ddef
20 changed files with 638 additions and 211 deletions

View File

@ -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',

View File

@ -71,7 +71,12 @@ export interface CODERoutes extends CDERoutes, ARRoutes {
toCODEHome: () => string
toCODESpaceAccessControl: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODESpaceSettings: (args: RequiredField<Pick<CODEProps, 'space' | 'settingSection'>, 'space'>) => string
toCODESpaceSettings: (
args: RequiredField<Pick<CODEProps, 'space' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'space'>
) => string
toCODEManageRepositories: (
args: RequiredField<Pick<CODEProps, 'space' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'space'>
) => string
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODEPipelineSettings: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
@ -104,7 +109,6 @@ export interface CODERoutes extends CDERoutes, ARRoutes {
args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
) => string
toCODESpaceSearch: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODESpaceLabels: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => 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 : ''

View File

@ -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
<Route
path={[
routes.toCODESpaceSettings({
space: pathProps.space,
settingSection: pathProps.settingSection,
settingSectionMode: pathProps.settingSectionMode,
ruleId: pathProps.ruleId
}),
routes.toCODESpaceSettings({
space: pathProps.space,
settingSection: pathProps.settingSection,
settingSectionMode: pathProps.settingSectionMode
}),
routes.toCODESpaceSettings({ space: pathProps.space, settingSection: pathProps.settingSection }),
routes.toCODESpaceSettings({ space: pathProps.space })
]}
@ -394,9 +405,26 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
</LayoutWithSideNav>
</Route>
<Route path={routes.toCODESpaceLabels({ space: pathProps.space })} exact>
<LayoutWithSideNav title={getString('labels.labels')}>
<ManageLabels />
<Route
path={[
routes.toCODEManageRepositories({
space: pathProps.space,
settingSection: pathProps.settingSection,
settingSectionMode: pathProps.settingSectionMode,
ruleId: pathProps.ruleId
}),
routes.toCODEManageRepositories({
space: pathProps.space,
settingSection: pathProps.settingSection,
settingSectionMode: pathProps.settingSectionMode
}),
routes.toCODEManageRepositories({ space: pathProps.space, settingSection: pathProps.settingSection }),
routes.toCODEManageRepositories({ space: pathProps.space })
]}
exact>
<LayoutWithSideNav title={getString('pageTitle.repositorySettings')}>
<ManageRepositories />
</LayoutWithSideNav>
</Route>

View File

@ -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<OpenapiRule>({
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<OpenapiRule>({
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<TypesPrincipalInfo[]>({
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<string[]>(usersArrayCurr)
const getUpdateChecksPath = () =>
currentRule?.scope === 0 && repoMetadata
? `/repos/${repoMetadata?.path}/+/checks/recent`
: `/spaces/${scopeRef}/+/checks/recent`
const { data: statuses } = useGet<string[]>({
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 (
<Formik<RulesFormPayload>
@ -392,7 +426,7 @@ const BranchProtectionForm = (props: {
flex={{ align: 'center-center' }}
padding={{ top: 'xxlarge', left: 'small' }}>
<SplitButton
// className={css.buttonContainer}
className={css.buttonContainer}
variation={ButtonVariation.TERTIARY}
text={
<Container flex={{ alignItems: 'center' }}>

View File

@ -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;
}

View File

@ -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

View File

@ -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 (
<Container className={css.main} padding="xlarge">
<Layout.Horizontal spacing="medium">
@ -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)}
/>
<Render when={showParentScopeFilter}>
<Checkbox
className={css.scopeCheckbox}
label={getString('branchProtection.showRulesScope')}
checked={inheritRules}
onChange={event => {
setInheritRules(event.currentTarget.checked)
}}
/>
</Render>
<FlexExpander />
<SearchInputWithSpinner
spinnerPosition="right"
@ -83,8 +106,11 @@ const BranchProtectionHeader = ({
export default BranchProtectionHeader
interface BranchProtectionHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
interface BranchProtectionHeaderProps extends Partial<Pick<GitInfoProps, 'repoMetadata'>> {
loading?: boolean
activeTab?: string
showParentScopeFilter: boolean
inheritRules: boolean
setInheritRules: (value: boolean) => void
onSearchTermChanged: (searchTerm: string) => void
}

View File

@ -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<PageBrowserProps>()
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<OpenapiRule>()
const { settingSection, ruleId, settingSectionMode } = useParams<CODEProps>()
const newRule = settingSection && settingSectionMode === SettingTypeMode.NEW
const editRule = settingSection !== '' && ruleId !== '' && settingSectionMode === SettingTypeMode.EDIT
const [showParentScopeFilter, setShowParentScopeFilter] = useState<boolean>(true)
const [inheritRules, setInheritRules] = useState<boolean>(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<OpenapiRule[]>({
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<OpenapiRule>[] = useMemo(
() => [
{
id: 'title',
width: '100%',
Cell: ({ row }: CellProps<OpenapiRule>) => {
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<boolean>(
row.original.state === 'active' || row.original.state === 'monitor' ? true : false
)
const { mutate } = useMutate<OpenapiRule>({
const { mutate: toggleRule } = useMutate<OpenapiRule>({
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 (
<Layout.Horizontal spacing="medium" padding={{ left: 'medium' }}>
<Container onClick={Utils.stopEvent}>
@ -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 }) => {
<Container padding={{ left: 'small' }} style={{ flexGrow: 1 }}>
<Layout.Horizontal spacing="small">
<Layout.Vertical>
<Text padding={{ right: 'small', top: 'xsmall' }} className={css.title}>
{row.original.identifier}
</Text>
<Layout.Horizontal
padding={{ right: 'small', top: 'xsmall' }}
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
{scopeIcon && <Icon padding={{ right: 'small' }} name={scopeIcon} size={16} />}
<Text className={css.title}>{row.original.identifier}</Text>
</Layout.Horizontal>
{!!row.original.description && (
<Text
lineClamp={4}
@ -374,14 +457,16 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
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 (
<Container>
<LoadingSpinner visible={loadingRules} />
{repoMetadata && !newRule && !editRule && (
{!newRule && !editRule && (
<BranchProtectionHeader
activeTab={activeTab}
showParentScopeFilter={showParentScopeFilter}
onSearchTermChanged={(value: React.SetStateAction<string>) => {
setSearchTerm(value)
setPage(1)
}}
repoMetadata={repoMetadata}
inheritRules={inheritRules}
setInheritRules={setInheritRules}
{...(repoMetadata && { repoMetadata: repoMetadata })}
/>
)}
{newRule || editRule ? (
<BranchProtectionForm
editMode={editRule}
repoMetadata={repoMetadata}
ruleUid={curRuleName}
currentRule={currentRule}
refetchRules={refetchRules}
settingSectionMode={settingSectionMode}
currentPageScope={currentPageScope}
/>
) : (
<Container padding="xlarge">
@ -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)}
/>

View File

@ -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

View File

@ -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}}'?"

View File

@ -37,7 +37,7 @@ const LabelsHeader = ({
//ToDo: check space permissions as well in case of spaces
return (
<Container className={css.main} padding={{ top: 'medium', right: 'xlarge', left: 'xlarge', bottom: 'medium' }}>
<Container className={css.main} padding={{ top: 'xlarge', right: 'xlarge', left: 'xlarge', bottom: 'medium' }}>
<Layout.Horizontal spacing="medium">
<Button
variation={ButtonVariation.PRIMARY}

View File

@ -1,22 +0,0 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.main {
min-height: calc(var(--page-height) - 160px);
background-color: var(--primary-bg) !important;
width: 100%;
margin: var(--spacing-small);
}

View File

@ -1,44 +0,0 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { PageBody, Page, Layout } from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import { useStrings } from 'framework/strings'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useAppContext } from 'AppContext'
import LabelsListing from 'pages/Labels/LabelsListing'
import { useGetCurrentPageScope } from 'hooks/useGetCurrentPageScope'
import css from './ManageLabels.module.scss'
export default function ManageLabels() {
const space = useGetSpaceParam()
const { hooks } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const pageScope = useGetCurrentPageScope()
const { getString } = useStrings()
return (
<Layout.Vertical className={css.main}>
<Page.Header title={getString('labels.labels')} />
<PageBody>
<Render when={!!isLabelEnabled}>
<LabelsListing currentPageScope={pageScope} space={space} />
</Render>
</PageBody>
</Layout.Vertical>
)
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.main {
min-height: calc(var(--page-height) - 160px);
background-color: var(--primary-bg) !important;
width: 100%;
margin: var(--spacing-small);
:global {
.bp3-tab {
width: fit-content !important;
height: 34px;
}
.bp3-tab-panel {
width: 100%;
}
.bp3-tab {
margin-top: 20px;
margin-bottom: unset !important;
}
.bp3-tab-list .bp3-tab[aria-selected='true'] {
background-color: var(--grey-0);
-webkit-box-shadow: none;
box-shadow: none;
border-bottom: 2px solid var(--primary-7);
border-bottom-left-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
.tabsContainer {
flex-grow: 1;
display: flex;
background-color: var(--primary-bg) !important;
> div {
flex-grow: 1;
display: flex;
flex-direction: column;
}
> div > div[role='tablist'] {
background-color: var(--white) !important;
padding-left: var(--spacing-large) !important;
padding-right: var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200) !important;
}
> div > div[role='tabpanel'] {
margin-top: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
}
[aria-selected='true'] {
.tabTitle,
.tabTitle:hover {
color: var(--grey-900) !important;
font-weight: 600 !important;
}
}
.tabTitle {
font-weight: 500;
color: var(--grey-700);
display: flex;
align-items: center;
height: 24px;
margin-top: var(--spacing-8);
> svg {
display: inline-block;
margin-right: 5px;
}
}
.tabTitle:not:first-child {
margin-left: var(--spacing-8) !important;
}
}

View File

@ -17,3 +17,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const main: string
export declare const tabsContainer: string
export declare const tabTitle: string

View File

@ -0,0 +1,80 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import cx from 'classnames'
import { Container, Tabs, Page } from '@harnessio/uicore'
import { useHistory, useParams } from 'react-router-dom'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import BranchProtectionListing from 'components/BranchProtection/BranchProtectionListing'
import { SettingsTab } from 'utils/GitUtils'
import LabelsListing from 'pages/Labels/LabelsListing'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions'
import { useGetCurrentPageScope } from 'hooks/useGetCurrentPageScope'
import css from './ManageRepositories.module.scss'
export default function ManageRepositories() {
const { settingSection } = useParams<CODEProps>()
const space = useGetSpaceParam()
const pageScope = useGetCurrentPageScope()
const history = useHistory()
const { routes, hooks, standalone } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const [activeTab, setActiveTab] = React.useState<string>(settingSection || SettingsTab.labels)
const { getString } = useStrings()
const tabListArray = [
...(isLabelEnabled || standalone
? [
{
id: SettingsTab.labels,
title: getString('labels.labels'),
panel: <LabelsListing activeTab={activeTab} currentPageScope={pageScope} space={space} />
}
]
: []),
{
id: SettingsTab.branchProtection,
title: getString('branchProtection.title'),
panel: <BranchProtectionListing activeTab={activeTab} currentPageScope={pageScope} />
}
]
return (
<Container className={css.main}>
<Page.Header title={getString('manageRepositories')} />
<Container className={cx(css.main, css.tabsContainer)}>
<Tabs
id="SettingsTabs"
large={false}
defaultSelectedTabId={activeTab}
animate={false}
onChange={(id: string) => {
setActiveTab(id)
history.replace(
routes.toCODEManageRepositories({
space,
settingSection: id !== SettingsTab.labels ? (id as string) : ''
})
)
}}
tabList={tabListArray}></Tabs>
</Container>
</Container>
)
}

View File

@ -66,16 +66,6 @@ export default function RepositorySettings() {
</Container>
)
},
{
id: SettingsTab.branchProtection,
title: getString('branchProtection.title'),
panel: <BranchProtectionListing activeTab={activeTab} />
},
{
id: SettingsTab.security,
title: getString('security'),
panel: <SecurityScanSettings repoMetadata={repoMetadata} activeTab={activeTab} />
},
...(isLabelEnabled || standalone
? [
{
@ -91,8 +81,23 @@ export default function RepositorySettings() {
)
}
]
: [])
: []),
{
id: SettingsTab.branchProtection,
title: getString('branchProtection.title'),
panel: (
<BranchProtectionListing
repoMetadata={repoMetadata}
activeTab={activeTab}
currentPageScope={LabelsPageScope.REPOSITORY}
/>
)
},
{
id: SettingsTab.security,
title: getString('security'),
panel: <SecurityScanSettings repoMetadata={repoMetadata} activeTab={activeTab} />
}
// {
// id: SettingsTab.webhooks,
// title: getString('webhooks'),

View File

@ -26,6 +26,7 @@ import { SettingsTab, SpaceSettingsTab } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import LabelsListing from 'pages/Labels/LabelsListing'
import { LabelsPageScope } from 'utils/Utils'
import BranchProtectionListing from 'components/BranchProtection/BranchProtectionListing'
import GeneralSpaceSettings from './GeneralSettings/GeneralSpaceSettings'
import css from './SpaceSettings.module.scss'
@ -51,6 +52,11 @@ export default function SpaceSettings() {
id: SettingsTab.labels,
title: getString('labels.labels'),
panel: <LabelsListing activeTab={activeTab} space={space} currentPageScope={LabelsPageScope.SPACE} />
},
{
id: SettingsTab.branchProtection,
title: getString('branchProtection.title'),
panel: <BranchProtectionListing activeTab={activeTab} currentPageScope={LabelsPageScope.SPACE} />
}
]
return (

View File

@ -610,6 +610,7 @@ export interface OpenapiRule {
description?: string
identifier?: string
pattern?: ProtectionPattern
scope?: number
state?: EnumRuleState
type?: OpenapiRuleType
updated?: number

View File

@ -40,6 +40,35 @@ export enum ACCESS_MODES {
EDIT
}
export enum ResourceType {
ACCOUNT = 'ACCOUNT',
ORGANIZATION = 'ORGANIZATION',
PROJECT = 'PROJECT',
CODE_REPOSITORY = 'CODE_REPOSITORY',
SPACE = 'SPACE'
}
export enum PermissionIdentifier {
CREATE_PROJECT = 'core_project_create',
UPDATE_PROJECT = 'core_project_edit',
DELETE_PROJECT = 'core_project_delete',
VIEW_PROJECT = 'core_project_view',
CREATE_ORG = 'core_organization_create',
UPDATE_ORG = 'core_organization_edit',
DELETE_ORG = 'core_organization_delete',
VIEW_ORG = 'core_organization_view',
CREATE_ACCOUNT = 'core_account_create',
UPDATE_ACCOUNT = 'core_account_edit',
DELETE_ACCOUNT = 'core_account_delete',
VIEW_ACCOUNT = 'core_account_view',
CODE_REPO_EDIT = 'code_repo_edit',
CODE_REPO_PUSH = 'code_repo_push',
CODE_REPO_VIEW = 'code_repo_view',
CODE_REPO_REVIEW = 'code_repo_review',
SPACE_VIEW = 'space_view',
SPACE_EDIT = 'space_edit'
}
export enum PullRequestSection {
CONVERSATION = 'conversation',
COMMITS = 'commits',
@ -901,6 +930,88 @@ export const getScopeData = (space: string, scope: number, standalone: boolean)
}
}
export const getEditPermissionRequestFromScope = (
space: string,
scope: number,
repoMetadata?: RepoRepositoryOutput
) => {
const [accountIdentifier, orgIdentifier, projectIdentifier] = space.split('/')
if (scope === 0 && repoMetadata) {
return {
resource: {
resourceType: ResourceType.CODE_REPOSITORY,
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: [PermissionIdentifier.CODE_REPO_EDIT]
}
} else {
switch (scope) {
case 1:
return {
resource: {
resourceType: ResourceType.ACCOUNT,
resourceIdentifier: accountIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_ACCOUNT]
}
case 2:
return {
resource: {
resourceType: ResourceType.ORGANIZATION,
resourceIdentifier: orgIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_ORG]
}
case 3:
return {
resource: {
resourceType: ResourceType.PROJECT,
resourceIdentifier: projectIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_PROJECT]
}
}
}
}
export const getEditPermissionRequestFromIdentifier = (space: string, repoMetadata?: RepoRepositoryOutput) => {
const [accountIdentifier, orgIdentifier, projectIdentifier] = space.split('/')
if (repoMetadata)
return {
resource: {
resourceType: ResourceType.CODE_REPOSITORY,
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: [PermissionIdentifier.CODE_REPO_EDIT]
}
else if (projectIdentifier) {
return {
resource: {
resourceType: ResourceType.PROJECT,
resourceIdentifier: projectIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_PROJECT]
}
} else if (orgIdentifier) {
return {
resource: {
resourceType: ResourceType.ORGANIZATION,
resourceIdentifier: orgIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_ORG]
}
} else
return {
resource: {
resourceType: ResourceType.ACCOUNT,
resourceIdentifier: accountIdentifier as string
},
permissions: [PermissionIdentifier.UPDATE_ACCOUNT]
}
}
export enum RuleFields {
APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',