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', './Webhooks': './src/pages/Webhooks/Webhooks.tsx',
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx', './WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
'./Search': './src/pages/Search/CodeSearchPage.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', './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx', './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx',
'./HAREnterpriseApp': './src/ar/app/EnterpriseApp.tsx', './HAREnterpriseApp': './src/ar/app/EnterpriseApp.tsx',

View File

@ -71,7 +71,12 @@ export interface CODERoutes extends CDERoutes, ARRoutes {
toCODEHome: () => string toCODEHome: () => string
toCODESpaceAccessControl: (args: Required<Pick<CODEProps, 'space'>>) => 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 toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODEPipelineSettings: (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'> args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
) => string ) => string
toCODESpaceSearch: (args: Required<Pick<CODEProps, 'space'>>) => string toCODESpaceSearch: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODESpaceLabels: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
@ -128,8 +132,14 @@ export const routes: CODERoutes = {
toCODEHome: () => `/`, toCODEHome: () => `/`,
toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`, toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`,
toCODESpaceSettings: ({ space, settingSection }) => toCODESpaceSettings: ({ space, settingSection, ruleId, settingSectionMode }) =>
`/settings/${space}/project${settingSection ? '/' + settingSection : ''}`, `/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`, toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`,
toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`, toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`,
toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`, toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`,
@ -160,7 +170,6 @@ export const routes: CODERoutes = {
toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`, toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`,
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`, toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
toCODETags: ({ repoPath }) => `/${repoPath}/tags`, toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
toCODESpaceLabels: ({ space }) => `/${space}/labels`,
toCODESettings: ({ repoPath, settingSection, ruleId, settingSectionMode }) => toCODESettings: ({ repoPath, settingSection, ruleId, settingSectionMode }) =>
`/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${ `/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${
settingSectionMode ? '/' + settingSectionMode : '' settingSectionMode ? '/' + settingSectionMode : ''

View File

@ -55,7 +55,7 @@ import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails' import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails'
import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing' import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing'
import GitspaceCreate from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate' 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')) const ArApp = lazy(() => import('@ar/gitness/ArApp'))
@ -87,6 +87,17 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
<Route <Route
path={[ 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, settingSection: pathProps.settingSection }),
routes.toCODESpaceSettings({ space: pathProps.space }) routes.toCODESpaceSettings({ space: pathProps.space })
]} ]}
@ -394,9 +405,26 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
</LayoutWithSideNav> </LayoutWithSideNav>
</Route> </Route>
<Route path={routes.toCODESpaceLabels({ space: pathProps.space })} exact> <Route
<LayoutWithSideNav title={getString('labels.labels')}> path={[
<ManageLabels /> 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> </LayoutWithSideNav>
</Route> </Route>

View File

@ -38,9 +38,12 @@ import { useGet, useMutate } from 'restful-react'
import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils' import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { import {
LabelsPageScope,
REGEX_VALID_REPO_NAME, REGEX_VALID_REPO_NAME,
RulesFormPayload, RulesFormPayload,
getEditPermissionRequestFromScope,
getErrorMessage, getErrorMessage,
getScopeData,
permissionProps, permissionProps,
rulesFormInitialPayload rulesFormInitialPayload
} from 'utils/Utils' } from 'utils/Utils'
@ -55,6 +58,7 @@ import type {
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { getConfig } from 'services/config'
import ProtectionRulesForm from './ProtectionRulesForm/ProtectionRulesForm' import ProtectionRulesForm from './ProtectionRulesForm/ProtectionRulesForm'
import Include from '../../../icons/Include.svg?url' import Include from '../../../icons/Include.svg?url'
import Exclude from '../../../icons/Exclude.svg?url' import Exclude from '../../../icons/Exclude.svg?url'
@ -62,33 +66,50 @@ import BypassList from './BypassList'
import css from './BranchProtectionForm.module.scss' import css from './BranchProtectionForm.module.scss'
const BranchProtectionForm = (props: { const BranchProtectionForm = (props: {
ruleUid: string currentRule?: OpenapiRule
editMode: boolean editMode: boolean
repoMetadata?: RepoRepositoryOutput | undefined repoMetadata?: RepoRepositoryOutput | undefined
refetchRules: () => void refetchRules: () => void
settingSectionMode: SettingTypeMode settingSectionMode: SettingTypeMode
currentPageScope: LabelsPageScope
}) => { }) => {
const { routes, routingId, standalone, hooks } = useAppContext() const { routes, routingId, standalone, hooks } = useAppContext()
const { ruleId } = useGetRepositoryMetadata() const { ruleId } = useGetRepositoryMetadata()
const { showError, showSuccess } = useToaster() 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 { getString } = useStrings()
const { data: rule } = useGet<OpenapiRule>({
path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${ruleId}`,
lazy: !repoMetadata && !ruleId
})
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [searchStatusTerm, setSearchStatusTerm] = 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({ const { mutate } = useMutate({
verb: 'POST', verb: 'POST',
path: `/api/v1/repos/${repoMetadata?.path}/+/rules/` base: getConfig('code/api/v1'),
path: getCreateRulePath()
}) })
const { mutate: updateRule } = useMutate({ const { mutate: updateRule } = useMutate({
verb: 'PATCH', verb: 'PATCH',
path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${ruleId}` base: getConfig('code/api/v1'),
path: getUpdateRulePath()
}) })
const { data: users } = useGet<TypesPrincipalInfo[]>({ const { data: users } = useGet<TypesPrincipalInfo[]>({
path: `/api/v1/principals`, path: `/api/v1/principals`,
@ -111,10 +132,19 @@ const BranchProtectionForm = (props: {
const usersArrayCurr = transformUserArray?.map(user => `${user.id} ${user.display_name}`) const usersArrayCurr = transformUserArray?.map(user => `${user.id} ${user.display_name}`)
const [userArrayState, setUserArrayState] = useState<string[]>(usersArrayCurr) 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[]>({ const { data: statuses } = useGet<string[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/checks/recent`, base: getConfig('code/api/v1'),
path: getUpdateChecksPath(),
queryParams: { queryParams: {
query: searchStatusTerm query: searchStatusTerm,
...(!repoMetadata && {
recursive: true
})
}, },
debounce: 500 debounce: 500
}) })
@ -141,10 +171,20 @@ const BranchProtectionForm = (props: {
showSuccess(successMessage) showSuccess(successMessage)
resetForm() resetForm()
history.push( history.push(
routes.toCODESettings({ repoMetadata
repoPath: repoMetadata?.path as string, ? routes.toCODESettings({
settingSection: SettingsTab.branchProtection repoPath: repoMetadata?.path as string,
}) settingSection: SettingsTab.branchProtection
})
: standalone
? routes.toCODESpaceSettings({
space,
settingSection: SettingsTab.branchProtection
})
: routes.toCODEManageRepositories({
space,
settingSection: SettingsTab.branchProtection
})
) )
refetchRules?.() refetchRules?.()
} catch (exception) { } catch (exception) {
@ -223,17 +263,11 @@ const BranchProtectionForm = (props: {
} }
return rulesFormInitialPayload // eslint-disable-next-line react-hooks/exhaustive-deps return rulesFormInitialPayload // eslint-disable-next-line react-hooks/exhaustive-deps
}, [editMode, rule, ruleUid, users]) }, [editMode, rule, currentRule, users])
const space = useGetSpaceParam()
const permPushResult = hooks?.usePermissionTranslate?.( const permPushResult = hooks?.usePermissionTranslate(
{ getEditPermissionRequestFromScope(space, currentRule?.scope ?? 0, repoMetadata),
resource: { [space, currentRule?.scope, repoMetadata]
resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_edit']
},
[space]
) )
return ( return (
<Formik<RulesFormPayload> <Formik<RulesFormPayload>
@ -392,7 +426,7 @@ const BranchProtectionForm = (props: {
flex={{ align: 'center-center' }} flex={{ align: 'center-center' }}
padding={{ top: 'xxlarge', left: 'small' }}> padding={{ top: 'xxlarge', left: 'small' }}>
<SplitButton <SplitButton
// className={css.buttonContainer} className={css.buttonContainer}
variation={ButtonVariation.TERTIARY} variation={ButtonVariation.TERTIARY}
text={ text={
<Container flex={{ alignItems: 'center' }}> <Container flex={{ alignItems: 'center' }}>

View File

@ -59,3 +59,10 @@
.cancelButton { .cancelButton {
margin-left: var(--spacing-small) !important; 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 main: string
export declare const noData: string export declare const noData: string
export declare const row: string export declare const row: string
export declare const scopeCheckbox: string
export declare const table: string export declare const table: string
export declare const title: string export declare const title: string
export declare const toggle: string export declare const toggle: string

View File

@ -15,19 +15,23 @@
*/ */
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import React, { useState } from 'react' 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 { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, SettingTypeMode } from 'utils/GitUtils' import { CodeIcon, GitInfoProps, SettingTypeMode } from 'utils/GitUtils'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { permissionProps } from 'utils/Utils' import { getEditPermissionRequestFromIdentifier, permissionProps } from 'utils/Utils'
import css from './BranchProtectionHeader.module.scss' import css from './BranchProtectionHeader.module.scss'
const BranchProtectionHeader = ({ const BranchProtectionHeader = ({
repoMetadata, repoMetadata,
loading, loading,
onSearchTermChanged, onSearchTermChanged,
activeTab activeTab,
showParentScopeFilter,
inheritRules,
setInheritRules
}: BranchProtectionHeaderProps) => { }: BranchProtectionHeaderProps) => {
const history = useHistory() const history = useHistory()
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
@ -37,17 +41,10 @@ const BranchProtectionHeader = ({
const space = useGetSpaceParam() const space = useGetSpaceParam()
const permPushResult = hooks?.usePermissionTranslate?.( const permPushResult = hooks?.usePermissionTranslate(getEditPermissionRequestFromIdentifier(space, repoMetadata), [
{ space,
resource: { repoMetadata
resourceType: 'CODE_REPOSITORY', ])
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_edit']
},
[space]
)
return ( return (
<Container className={css.main} padding="xlarge"> <Container className={css.main} padding="xlarge">
<Layout.Horizontal spacing="medium"> <Layout.Horizontal spacing="medium">
@ -56,16 +53,42 @@ const BranchProtectionHeader = ({
text={getString('branchProtection.newRule')} text={getString('branchProtection.newRule')}
icon={CodeIcon.Add} icon={CodeIcon.Add}
onClick={() => onClick={() =>
history.push( repoMetadata
routes.toCODESettings({ ? history.push(
repoPath: repoMetadata?.path as string, routes.toCODESettings({
settingSection: activeTab, repoPath: repoMetadata?.path as string,
settingSectionMode: SettingTypeMode.NEW 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)} {...permissionProps(permPushResult, standalone)}
/> />
<Render when={showParentScopeFilter}>
<Checkbox
className={css.scopeCheckbox}
label={getString('branchProtection.showRulesScope')}
checked={inheritRules}
onChange={event => {
setInheritRules(event.currentTarget.checked)
}}
/>
</Render>
<FlexExpander /> <FlexExpander />
<SearchInputWithSpinner <SearchInputWithSpinner
spinnerPosition="right" spinnerPosition="right"
@ -83,8 +106,11 @@ const BranchProtectionHeader = ({
export default BranchProtectionHeader export default BranchProtectionHeader
interface BranchProtectionHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> { interface BranchProtectionHeaderProps extends Partial<Pick<GitInfoProps, 'repoMetadata'>> {
loading?: boolean loading?: boolean
activeTab?: string activeTab?: string
showParentScopeFilter: boolean
inheritRules: boolean
setInheritRules: (value: boolean) => void
onSearchTermChanged: (searchTerm: string) => void onSearchTermChanged: (searchTerm: string) => void
} }

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import React, { useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { import {
Container, Container,
Layout, Layout,
@ -35,8 +35,8 @@ import type { CellProps, Column } from 'react-table'
import { useGet, useMutate } from 'restful-react' import { useGet, useMutate } from 'restful-react'
import { FontVariation } from '@harnessio/design-system' import { FontVariation } from '@harnessio/design-system'
import { Position } from '@blueprintjs/core' import { Position } from '@blueprintjs/core'
import { useHistory } from 'react-router-dom' import { useHistory, useParams } from 'react-router-dom'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { Icon } from '@harnessio/icons'
import { useQueryParams } from 'hooks/useQueryParams' import { useQueryParams } from 'hooks/useQueryParams'
import { usePageIndex } from 'hooks/usePageIndex' import { usePageIndex } from 'hooks/usePageIndex'
import { import {
@ -47,7 +47,12 @@ import {
Rule, Rule,
RuleFields, RuleFields,
BranchProtectionRulesMapType, BranchProtectionRulesMapType,
createRuleFieldsMap createRuleFieldsMap,
LabelsPageScope,
getScopeData,
getScopeIcon,
getEditPermissionRequestFromScope,
getEditPermissionRequestFromIdentifier
} from 'utils/Utils' } from 'utils/Utils'
import { SettingTypeMode } from 'utils/GitUtils' import { SettingTypeMode } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
@ -56,46 +61,70 @@ import { useStrings } from 'framework/strings'
import { useConfirmAct } from 'hooks/useConfirmAction' import { useConfirmAct } from 'hooks/useConfirmAction'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' 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 { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import type { CODEProps } from 'RouteDefinitions'
import { getConfig } from 'services/config'
import Include from '../../icons/Include.svg?url' import Include from '../../icons/Include.svg?url'
import Exclude from '../../icons/Exclude.svg?url' import Exclude from '../../icons/Exclude.svg?url'
import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm' import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm'
import BranchProtectionHeader from './BranchProtectionHeader/BranchProtectionHeader' import BranchProtectionHeader from './BranchProtectionHeader/BranchProtectionHeader'
import css from './BranchProtectionListing.module.scss' import css from './BranchProtectionListing.module.scss'
const BranchProtectionListing = (props: { activeTab: string }) => { const BranchProtectionListing = (props: {
const { activeTab } = props activeTab: string
repoMetadata?: RepoRepositoryOutput
currentPageScope: LabelsPageScope
}) => {
const { activeTab, repoMetadata, currentPageScope } = props
const { getString } = useStrings() const { getString } = useStrings()
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const history = useHistory() const history = useHistory()
const { routes } = useAppContext() const { routes, standalone, hooks } = useAppContext()
const pageBrowser = useQueryParams<PageBrowserProps>() const pageBrowser = useQueryParams<PageBrowserProps>()
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
const [page, setPage] = usePageIndex(pageInit) const [page, setPage] = usePageIndex(pageInit)
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [curRuleName, setCurRuleName] = useState('') const [currentRule, setCurrentRule] = useState<OpenapiRule>()
const { repoMetadata, settingSection, ruleId, settingSectionMode } = useGetRepositoryMetadata() const { settingSection, ruleId, settingSectionMode } = useParams<CODEProps>()
const newRule = settingSection && settingSectionMode === SettingTypeMode.NEW const newRule = settingSection && settingSectionMode === SettingTypeMode.NEW
const editRule = settingSection !== '' && ruleId !== '' && settingSectionMode === SettingTypeMode.EDIT 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 { const {
data: rules, data: rules,
refetch: refetchRules, refetch: refetchRules,
loading: loadingRules, loading: loadingRules,
response response
} = useGet<OpenapiRule[]>({ } = useGet<OpenapiRule[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/rules`, base: getConfig('code/api/v1'),
path: getRulesPath(),
queryParams: { queryParams: {
limit: LIST_FETCHING_LIMIT, limit: LIST_FETCHING_LIMIT,
inherited: inheritRules,
page, page,
sort: 'date', sort: 'date',
order: 'desc', order: 'desc',
query: searchTerm query: searchTerm
}, },
debounce: 500, debounce: 500,
lazy: !repoMetadata || !!editRule lazy: !!editRule
}) })
const branchProtectionRules = { 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( const columns: Column<OpenapiRule>[] = useMemo(
() => [ () => [
{ {
id: 'title', id: 'title',
width: '100%', width: '100%',
Cell: ({ row }: CellProps<OpenapiRule>) => { 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>( const [checked, setChecked] = useState<boolean>(
row.original.state === 'active' || row.original.state === 'monitor' ? true : false row.original.state === 'active' || row.original.state === 'monitor' ? true : false
) )
const { mutate } = useMutate<OpenapiRule>({ const { mutate: toggleRule } = useMutate<OpenapiRule>({
verb: 'PATCH', 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 [popoverDialogOpen, setPopoverDialogOpen] = useState(false)
const { mutate: deleteRule } = useMutate({ const { mutate: deleteRule } = useMutate({
verb: 'DELETE', verb: 'DELETE',
path: `/api/v1/repos/${repoMetadata?.path}/+/rules/${row.original.identifier}` base: getConfig('code/api/v1'),
path: getRuleIDPath()
}) })
const confirmDelete = useConfirmAct() const confirmDelete = useConfirmAct()
const includeElements = (row.original?.pattern as ProtectionPattern)?.include?.map( 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 nonEmptyRules = checkAppliedRules(row.original.definition as Rule, branchProtectionRules)
const { hooks, standalone } = useAppContext() const scope = row.original?.scope
const permPushResult = hooks?.usePermissionTranslate(
const space = useGetSpaceParam() getEditPermissionRequestFromScope(space, scope ?? 0, repoMetadata),
[space, repoMetadata, scope]
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_edit']
},
[space]
) )
const scopeIcon = getScopeIcon(row.original?.scope, standalone)
return ( return (
<Layout.Horizontal spacing="medium" padding={{ left: 'medium' }}> <Layout.Horizontal spacing="medium" padding={{ left: 'medium' }}>
<Container onClick={Utils.stopEvent}> <Container onClick={Utils.stopEvent}>
@ -313,7 +394,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
text={getString('confirm')} text={getString('confirm')}
onClick={() => { onClick={() => {
const data = { state: checked ? 'disabled' : 'active' } const data = { state: checked ? 'disabled' : 'active' }
mutate(data) toggleRule(data)
.then(() => { .then(() => {
showSuccess(getString('branchProtection.ruleUpdated')) showSuccess(getString('branchProtection.ruleUpdated'))
}) })
@ -348,10 +429,12 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
<Container padding={{ left: 'small' }} style={{ flexGrow: 1 }}> <Container padding={{ left: 'small' }} style={{ flexGrow: 1 }}>
<Layout.Horizontal spacing="small"> <Layout.Horizontal spacing="small">
<Layout.Vertical> <Layout.Vertical>
<Text padding={{ right: 'small', top: 'xsmall' }} className={css.title}> <Layout.Horizontal
{row.original.identifier} padding={{ right: 'small', top: 'xsmall' }}
</Text> 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 && ( {!!row.original.description && (
<Text <Text
lineClamp={4} lineClamp={4}
@ -374,14 +457,16 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
iconName: 'Edit', iconName: 'Edit',
text: getString('branchProtection.editRule'), text: getString('branchProtection.editRule'),
onClick: () => { onClick: () => {
history.push( setCurrentRule(row.original)
routes.toCODESettings({ navigateToSettings({
repoPath: repoMetadata?.path as string, repoMetadata,
settingSection: settingSection, standalone,
settingSectionMode: SettingTypeMode.EDIT, space,
ruleId: String(row.original.identifier) 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 ], // eslint-disable-next-line react-hooks/exhaustive-deps
[history, getString, repoMetadata?.path, setPage, showError, showSuccess] [history, getString, repoMetadata?.path, setPage, showError, showSuccess]
) )
const { hooks, standalone } = useAppContext()
const space = useGetSpaceParam() const permPushResult = hooks?.usePermissionTranslate(getEditPermissionRequestFromIdentifier(space, repoMetadata), [
space,
const permPushResult = hooks?.usePermissionTranslate?.( repoMetadata
{ ])
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_edit']
},
[space]
)
return ( return (
<Container> <Container>
<LoadingSpinner visible={loadingRules} /> <LoadingSpinner visible={loadingRules} />
{repoMetadata && !newRule && !editRule && ( {!newRule && !editRule && (
<BranchProtectionHeader <BranchProtectionHeader
activeTab={activeTab} activeTab={activeTab}
showParentScopeFilter={showParentScopeFilter}
onSearchTermChanged={(value: React.SetStateAction<string>) => { onSearchTermChanged={(value: React.SetStateAction<string>) => {
setSearchTerm(value) setSearchTerm(value)
setPage(1) setPage(1)
}} }}
repoMetadata={repoMetadata} inheritRules={inheritRules}
setInheritRules={setInheritRules}
{...(repoMetadata && { repoMetadata: repoMetadata })}
/> />
)} )}
{newRule || editRule ? ( {newRule || editRule ? (
<BranchProtectionForm <BranchProtectionForm
editMode={editRule} editMode={editRule}
repoMetadata={repoMetadata} repoMetadata={repoMetadata}
ruleUid={curRuleName} currentRule={currentRule}
refetchRules={refetchRules} refetchRules={refetchRules}
settingSectionMode={settingSectionMode} settingSectionMode={settingSectionMode}
currentPageScope={currentPageScope}
/> />
) : ( ) : (
<Container padding="xlarge"> <Container padding="xlarge">
@ -503,15 +583,16 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
data={rules} data={rules}
getRowClassName={() => css.row} getRowClassName={() => css.row}
onRowClick={row => { onRowClick={row => {
setCurRuleName(row.identifier as string) setCurrentRule(row)
history.push( navigateToSettings({
routes.toCODESettings({ repoMetadata,
repoPath: repoMetadata?.path as string, standalone,
settingSection: settingSection, space,
settingSectionMode: SettingTypeMode.EDIT, scope: row.scope,
ruleId: String(row.identifier) settingSection,
}) settingSectionMode: SettingTypeMode.EDIT,
) ruleId: String(row.identifier)
})
}} }}
/> />
@ -525,13 +606,13 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
message={getString('branchProtection.ruleEmpty')} message={getString('branchProtection.ruleEmpty')}
buttonText={getString('branchProtection.newRule')} buttonText={getString('branchProtection.newRule')}
onButtonClick={() => { onButtonClick={() => {
history.push( navigateToSettings({
routes.toCODESettings({ repoMetadata,
repoPath: repoMetadata?.path as string, standalone,
settingSection: activeTab, space,
settingSectionMode: SettingTypeMode.NEW settingSection,
}) settingSectionMode: SettingTypeMode.NEW
) })
}} }}
permissionProp={permissionProps(permPushResult, standalone)} permissionProp={permissionProps(permPushResult, standalone)}
/> />

View File

@ -129,6 +129,7 @@ export interface StringsMap {
'branchProtection.ruleEmpty': string 'branchProtection.ruleEmpty': string
'branchProtection.ruleUpdated': string 'branchProtection.ruleUpdated': string
'branchProtection.saveRule': string 'branchProtection.saveRule': string
'branchProtection.showRulesScope': string
'branchProtection.statusCheck': string 'branchProtection.statusCheck': string
'branchProtection.targetBranches': string 'branchProtection.targetBranches': string
'branchProtection.targetPatternHint': string 'branchProtection.targetPatternHint': string
@ -592,6 +593,7 @@ export interface StringsMap {
makeRequired: string makeRequired: string
manageApiToken: string manageApiToken: string
manageCredText: string manageCredText: string
manageRepositories: string
manageRepository: string manageRepository: string
markAsDraft: string markAsDraft: string
matchPassword: string matchPassword: string

View File

@ -31,6 +31,7 @@ commitChanges: Commit changes
pullRequests: Pull Requests pullRequests: Pull Requests
settings: Settings settings: Settings
manageRepository: Manage Repository manageRepository: Manage Repository
manageRepositories: Manage Repositories
newFile: New File newFile: New File
editFile: Edit File editFile: Edit File
prev: Prev prev: Prev
@ -990,7 +991,7 @@ branchProtection:
targetPatternHint: Match branches using globstar patterns (e.g. "golden", "feature-*", "releases/**") targetPatternHint: Match branches using globstar patterns (e.g. "golden", "feature-*", "releases/**")
defaultBranch: Default branch defaultBranch: Default branch
bypassList: Bypass List bypassList: Bypass List
newRule: New branch rule newRule: New Branch Rule
allRepoOwners: Allow users with edit permission on the repository to bypass allRepoOwners: Allow users with edit permission on the repository to bypass
protectionSelectAll: 'Rules: Select all that apply' protectionSelectAll: 'Rules: Select all that apply'
requireMinReviewersTitle: Require a minimum number of reviewers requireMinReviewersTitle: Require a minimum number of reviewers
@ -1025,6 +1026,7 @@ branchProtection:
deleteRule: Delete Rule deleteRule: Delete Rule
ruleDeleted: Rule Deleted ruleDeleted: Rule Deleted
ruleEmpty: There are no rules in your repo. Click the button below to create a rule. 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 createRule: Create rule
deleteProtectionRule: Delete protection rule deleteProtectionRule: Delete protection rule
deleteText: "Are you sure to delete the rule, '{{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 //ToDo: check space permissions as well in case of spaces
return ( 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"> <Layout.Horizontal spacing="medium">
<Button <Button
variation={ButtonVariation.PRIMARY} 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 */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const main: string 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> </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 ...(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, // id: SettingsTab.webhooks,
// title: getString('webhooks'), // title: getString('webhooks'),

View File

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

View File

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

View File

@ -40,6 +40,35 @@ export enum ACCESS_MODES {
EDIT 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 { export enum PullRequestSection {
CONVERSATION = 'conversation', CONVERSATION = 'conversation',
COMMITS = 'commits', 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 { export enum RuleFields {
APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count', APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners', APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',