fix: public repo config and labels flicker (#2571)

* fix lint
* label fixes and minor ui fix
* udpate: label values api to use query params
* fix: public repo config and labels flicker
pull/3545/head
Ritik Kapoor 2024-08-28 08:14:43 +00:00 committed by Harness
parent e18a8c410f
commit 81b10ba01c
33 changed files with 560 additions and 421 deletions

View File

@ -72,6 +72,7 @@ export interface AppProps {
useLogsStreaming: Unknown useLogsStreaming: Unknown
useFeatureFlags: Unknown useFeatureFlags: Unknown
useGetSettingValue: Unknown useGetSettingValue: Unknown
useGetAuthSettings: Unknown
useGetUserSourceCodeManagers?: Unknown useGetUserSourceCodeManagers?: Unknown
useListAggregatedTokens?: Unknown useListAggregatedTokens?: Unknown
useDeleteToken?: Unknown useDeleteToken?: Unknown

View File

@ -21,6 +21,7 @@ import { routes } from 'RouteDefinitions'
import { defaultCurrentUser } from 'AppContext' import { defaultCurrentUser } from 'AppContext'
import { useFeatureFlags } from 'hooks/useFeatureFlag' import { useFeatureFlags } from 'hooks/useFeatureFlag'
import { useGetSettingValue } from 'hooks/useGetSettingValue' import { useGetSettingValue } from 'hooks/useGetSettingValue'
import { useGetAuthSettings } from 'hooks/useGetAuthSettings'
import { defaultUsefulOrNot } from 'components/DefaultUsefulOrNot/UsefulOrNot' import { defaultUsefulOrNot } from 'components/DefaultUsefulOrNot/UsefulOrNot'
import App from './App' import App from './App'
import './bootstrap.scss' import './bootstrap.scss'
@ -41,7 +42,8 @@ ReactDOM.render(
useLogsContent: noop, useLogsContent: noop,
useLogsStreaming: noop, useLogsStreaming: noop,
useFeatureFlags, useFeatureFlags,
useGetSettingValue useGetSettingValue,
useGetAuthSettings
}} }}
currentUser={defaultCurrentUser} currentUser={defaultCurrentUser}
customComponents={{ customComponents={{

View File

@ -93,6 +93,6 @@
} }
.valuesList { .valuesList {
flex-wrap: wrap; flex-wrap: wrap;
gap: 7px; gap: 8px;
width: 100%; width: 100%;
} }

View File

@ -19,7 +19,7 @@ import cx from 'classnames'
import { Button, ButtonSize, ButtonVariation, Container, Layout, Tag, Text } from '@harnessio/uicore' import { Button, ButtonSize, ButtonVariation, Container, Layout, Tag, Text } from '@harnessio/uicore'
import { FontVariation } from '@harnessio/design-system' import { FontVariation } from '@harnessio/design-system'
import { useGet } from 'restful-react' import { useGet } from 'restful-react'
import { Menu } from '@blueprintjs/core' import { Menu, Spinner } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { isEmpty } from 'lodash-es' import { isEmpty } from 'lodash-es'
import { ColorName, LabelType, getColorsObj, getScopeData, getScopeIcon } from 'utils/Utils' import { ColorName, LabelType, getColorsObj, getScopeData, getScopeIcon } from 'utils/Utils'
@ -329,7 +329,59 @@ export const LabelValuesList: React.FC<{
/> />
)) ))
) : ( ) : (
<Icon name="steps-spinner" size={16} /> <Spinner size={16} />
)}
</Layout.Horizontal>
)
}
// ToDo : Remove LabelValuesListQuery component when Encoding is handled by BE for Harness
export const LabelValuesListQuery: React.FC<{
name: string
scope: number
repoMetadata?: RepoRepositoryOutput
space?: string
standalone: boolean
}> = ({ name, scope, repoMetadata, space = '', standalone }) => {
const { scopeRef } = getScopeData(space as string, scope, standalone)
const getPath = () =>
scope === 0
? `/repos/${repoMetadata?.identifier}/labels/${encodeURIComponent(name)}/values`
: `/labels/${encodeURIComponent(name)}/values`
const {
data: labelValues,
refetch: refetchLabelValues,
loading: loadingLabelValues
} = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'),
path: getPath(),
queryParams: {
accountIdentifier: scopeRef?.split('/')[0],
orgIdentifier: scopeRef?.split('/')[1],
projectIdentifier: scopeRef?.split('/')[2]
},
lazy: true
})
useEffect(() => {
refetchLabelValues()
}, [name, scope, space, repoMetadata])
return (
<Layout.Horizontal className={css.valuesList}>
{!loadingLabelValues && labelValues && !isEmpty(labelValues) ? (
labelValues.map(value => (
<Label
key={`${name}-${value.value}`}
name={name}
scope={scope}
label_value={{ name: value.value, color: value.color as ColorName }}
/>
))
) : (
<Spinner size={16} />
)} )}
</Layout.Horizontal> </Layout.Horizontal>
) )

View File

@ -129,14 +129,13 @@ export const LabelFilter = (props: LabelFilterProps) => {
const getLabelValuesPromise = async (key: string, scope: number): Promise<SelectOption[]> => { const getLabelValuesPromise = async (key: string, scope: number): Promise<SelectOption[]> => {
setLoadingLabelValues(true) setLoadingLabelValues(true)
const { scopeRef } = getScopeData(spaceRef, scope, standalone) const { scopeRef } = getScopeData(spaceRef, scope, standalone)
if (scope === 0) { const getPath = () =>
scope === 0
? `/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(key)}/values`
: `/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(key)}/values`
try { try {
const fetchedValues: TypesLabelValue[] = await getUsingFetch( const fetchedValues: TypesLabelValue[] = await getUsingFetch(getConfig('code/api/v1'), getPath(), bearerToken, {})
getConfig('code/api/v1'),
`/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(key)}/values`,
bearerToken,
{}
)
const updatedValuesList = mapToSelectOptions(fetchedValues) const updatedValuesList = mapToSelectOptions(fetchedValues)
setLoadingLabelValues(false) setLoadingLabelValues(false)
return updatedValuesList return updatedValuesList
@ -145,22 +144,26 @@ export const LabelFilter = (props: LabelFilterProps) => {
showError(getErrorMessage(error)) showError(getErrorMessage(error))
return [] return []
} }
} else { }
// ToDo : Remove getLabelValuesPromiseQuery component when Encoding is handled by BE for Harness
const getLabelValuesPromiseQuery = async (key: string, scope: number): Promise<SelectOption[]> => {
setLoadingLabelValues(true)
const { scopeRef } = getScopeData(spaceRef, scope, standalone)
const getPath = () =>
scope === 0
? `/repos/${repoMetadata?.identifier}/labels/${encodeURIComponent(key)}/values`
: `/labels/${encodeURIComponent(key)}/values`
try { try {
const fetchedValues: TypesLabelValue[] = await getUsingFetch( const fetchedValues: TypesLabelValue[] = await getUsingFetch(getConfig('code/api/v1'), getPath(), bearerToken, {
getConfig('code/api/v1'), queryParams: {
`/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(key)}/values`, accountIdentifier: scopeRef?.split('/')[0],
bearerToken, orgIdentifier: scopeRef?.split('/')[1],
{} projectIdentifier: scopeRef?.split('/')[2]
) }
const updatedValuesList = Array.isArray(fetchedValues) })
? ([ const updatedValuesList = mapToSelectOptions(fetchedValues)
...(fetchedValues || []).map(item => ({
label: JSON.stringify(item),
value: String(item?.id)
}))
] as SelectOption[])
: ([] as SelectOption[])
setLoadingLabelValues(false) setLoadingLabelValues(false)
return updatedValuesList return updatedValuesList
} catch (error) { } catch (error) {
@ -169,7 +172,6 @@ export const LabelFilter = (props: LabelFilterProps) => {
return [] return []
} }
} }
}
const containsFilter = (filterObjArr: LabelFilterObj[], currentObj: any, type: utilFilterType) => { const containsFilter = (filterObjArr: LabelFilterObj[], currentObj: any, type: utilFilterType) => {
let res = false let res = false
@ -320,11 +322,20 @@ export const LabelFilter = (props: LabelFilterProps) => {
setIsVisible(true) setIsVisible(true)
setValueQuery('') setValueQuery('')
setHighlightItem(item.label as string) setHighlightItem(item.label as string)
// ToDo : Remove this check once BE has support for encoding
if (standalone) {
getLabelValuesPromise(itemObj.key, itemObj.scope) getLabelValuesPromise(itemObj.key, itemObj.scope)
.then(res => setLabelValues(res)) .then(res => setLabelValues(res))
.catch(err => { .catch(err => {
showError(getErrorMessage(err)) showError(getErrorMessage(err))
}) })
} else {
getLabelValuesPromiseQuery(itemObj.key, itemObj.scope)
.then(res => setLabelValues(res))
.catch(err => {
showError(getErrorMessage(err))
})
}
}} }}
tooltip={ tooltip={
labelsValueList && !loadingLabelValues ? ( labelsValueList && !loadingLabelValues ? (

View File

@ -56,6 +56,7 @@ export interface LabelSelectorProps {
setQuery: React.Dispatch<React.SetStateAction<string>> setQuery: React.Dispatch<React.SetStateAction<string>>
query: string query: string
labelListLoading: boolean labelListLoading: boolean
refetchActivities: () => void
} }
export interface LabelSelectProps extends Omit<ButtonProps, 'onSelect'> { export interface LabelSelectProps extends Omit<ButtonProps, 'onSelect'> {
@ -87,6 +88,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
query, query,
setQuery, setQuery,
labelListLoading, labelListLoading,
refetchActivities,
...props ...props
}) => { }) => {
const [popoverDialogOpen, setPopoverDialogOpen] = useState<boolean>(false) const [popoverDialogOpen, setPopoverDialogOpen] = useState<boolean>(false)
@ -110,7 +112,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
resourceType: 'CODE_REPOSITORY', resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string resourceIdentifier: repoMetadata?.identifier as string
}, },
permissions: ['code_repo_edit'] permissions: ['code_repo_push']
}, },
[space] [space]
) )
@ -121,6 +123,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
text={<span className={css.prefix}>{getString('add')}</span>} text={<span className={css.prefix}>{getString('add')}</span>}
variation={ButtonVariation.TERTIARY} variation={ButtonVariation.TERTIARY}
minimal minimal
disabled={labelListLoading}
size={ButtonSize.SMALL} size={ButtonSize.SMALL}
tooltip={ tooltip={
<PopoverContent <PopoverContent
@ -139,6 +142,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
setQuery('') setQuery('')
setPopoverDialogOpen(false) setPopoverDialogOpen(false)
showSuccess(`Applied '${label.key}' label`) showSuccess(`Applied '${label.key}' label`)
refetchActivities()
}) })
.catch(error => showError(getErrorMessage(error))) .catch(error => showError(getErrorMessage(error)))
} catch (exception) { } catch (exception) {
@ -161,6 +165,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
setCurrentLabel({ key: '', id: -1 }) setCurrentLabel({ key: '', id: -1 })
setPopoverDialogOpen(false) setPopoverDialogOpen(false)
showSuccess(`Applied '${labelKey}:${valueKey}' label`) showSuccess(`Applied '${labelKey}:${valueKey}' label`)
refetchActivities()
}) })
.catch(error => showError(getErrorMessage(error))) .catch(error => showError(getErrorMessage(error)))
} catch (exception) { } catch (exception) {
@ -185,6 +190,7 @@ export const LabelSelector: React.FC<LabelSelectorProps> = ({
setCurrentLabel({ key: '', id: -1 }) setCurrentLabel({ key: '', id: -1 })
setPopoverDialogOpen(false) setPopoverDialogOpen(false)
setQuery('') setQuery('')
refetchActivities()
}) })
.catch(error => showError(getErrorMessage(error))) .catch(error => showError(getErrorMessage(error)))
} catch (exception) { } catch (exception) {
@ -266,60 +272,62 @@ const PopoverContent: React.FC<LabelSelectProps> = ({
} }
}, [menuItemIndex, menuState, labelsList, labelsValueList]) }, [menuItemIndex, menuState, labelsList, labelsValueList])
const handleKeyDownLabels: React.KeyboardEventHandler<HTMLInputElement> = e => { // const handleKeyDownLabels: React.KeyboardEventHandler<HTMLInputElement> = e => {
if (labelsList && labelsList.length !== 0) { // if (labelsList && labelsList.length !== 0) {
switch (e.key) { // e.preventDefault()
case 'ArrowDown': // switch (e.key) {
setMenuItemIndex((index: number) => { // case 'ArrowDown':
return index + 1 > labelsList.length ? 1 : index + 1 // setMenuItemIndex((index: number) => {
}) // return index + 1 > labelsList.length ? 1 : index + 1
break // })
case 'ArrowUp': // break
setMenuItemIndex((index: number) => { // case 'ArrowUp':
return index - 1 > 0 ? index - 1 : labelsList.length // setMenuItemIndex((index: number) => {
}) // return index - 1 > 0 ? index - 1 : labelsList.length
break // })
case 'Enter': // break
if (labelsList[menuItemIndex - 1]) { // case 'Enter':
onSelectLabel(labelsList[menuItemIndex - 1]) // if (labelsList[menuItemIndex - 1]) {
setQuery('') // onSelectLabel(labelsList[menuItemIndex - 1])
} // setQuery('')
break // }
default: // break
break // default:
} // break
} // }
} // }
// }
const handleKeyDownValue: React.KeyboardEventHandler<HTMLInputElement> = e => { const handleKeyDownValue: React.KeyboardEventHandler<HTMLInputElement> = e => {
if (e.key === 'Backspace' && !query && currentLabel) { if (e.key === 'Backspace' && !query && currentLabel) {
setQuery('') setQuery('')
handleValueRemove && handleValueRemove() handleValueRemove && handleValueRemove()
} else if (labelsValueList && labelsValueList.length !== 0) {
switch (e.key) {
case 'ArrowDown':
setMenuItemIndex((index: number) => {
return index + 1 > labelsValueList.length ? 1 : index + 1
})
break
case 'ArrowUp':
setMenuItemIndex((index: number) => {
return index - 1 > 0 ? index - 1 : labelsValueList.length
})
break
case 'Enter':
onSelectValue(
currentLabel.id ?? -1,
labelsValueList[menuItemIndex - 1].id ?? -1,
currentLabel.key ?? '',
labelsValueList[menuItemIndex - 1].value ?? ''
)
setQuery('')
break
default:
break
}
} }
// } else if (labelsValueList && labelsValueList.length !== 0) {
// switch (e.key) {
// case 'ArrowDown':
// setMenuItemIndex((index: number) => {
// return index + 1 > labelsValueList.length ? 1 : index + 1
// })
// break
// case 'ArrowUp':
// setMenuItemIndex((index: number) => {
// return index - 1 > 0 ? index - 1 : labelsValueList.length
// })
// break
// case 'Enter':
// onSelectValue(
// currentLabel.id ?? -1,
// labelsValueList[menuItemIndex - 1].id ?? -1,
// currentLabel.key ?? '',
// labelsValueList[menuItemIndex - 1].value ?? ''
// )
// setQuery('')
// break
// default:
// break
// }
// }
} }
return ( return (
@ -343,7 +351,7 @@ const PopoverContent: React.FC<LabelSelectProps> = ({
className: css.closeBtn, className: css.closeBtn,
size: 20 size: 20
}} }}
onKeyDown={handleKeyDownLabels} // onKeyDown={handleKeyDownLabels}
/> />
) : ( ) : (
currentLabel && currentLabel &&

View File

@ -76,6 +76,7 @@ import type {
} from 'services/code' } from 'services/code'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import type { TypesRepository } from 'cde-gitness/services' import type { TypesRepository } from 'cde-gitness/services'
import { usePublicResourceConfig } from 'hooks/usePublicResourceConfig'
import ImportForm from './ImportForm/ImportForm' import ImportForm from './ImportForm/ImportForm'
import ImportReposForm from './ImportReposForm/ImportReposForm' import ImportReposForm from './ImportReposForm/ImportReposForm'
import Private from '../../icons/private.svg?url' import Private from '../../icons/private.svg?url'
@ -112,7 +113,8 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
const ModalComponent: React.FC = () => { const ModalComponent: React.FC = () => {
const { getString } = useStrings() const { getString } = useStrings()
const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME) const [branchName, setBranchName] = useState(DEFAULT_BRANCH_NAME)
const [enablePublicRepo, setEnablePublicRepo] = useState(false) const { allowPublicResourceCreation, configLoading, systemConfigError, errorWhileFetchingAuthSettings } =
usePublicResourceConfig()
const { showError } = useToaster() const { showError } = useToaster()
const { mutate: createRepo, loading: submitLoading } = useMutate<RepoRepositoryOutput>({ const { mutate: createRepo, loading: submitLoading } = useMutate<RepoRepositoryOutput>({
@ -147,31 +149,19 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
loading: licenseLoading, loading: licenseLoading,
error: licenseError error: licenseError
} = useGet({ path: '/api/v1/resources/license' }) } = useGet({ path: '/api/v1/resources/license' })
const {
data: systemConfig,
loading: systemConfigLoading,
error: systemConfigError
} = useGet({ path: 'api/v1/system/config' })
const loading = const loading =
submitLoading || submitLoading || gitIgnoreLoading || licenseLoading || importRepoLoading || submitImportLoading || configLoading
gitIgnoreLoading ||
licenseLoading ||
importRepoLoading ||
submitImportLoading ||
systemConfigLoading
useEffect(() => { useEffect(() => {
if (gitIgnoreError || licenseError || systemConfigError) { if (gitIgnoreError || licenseError || systemConfigError || errorWhileFetchingAuthSettings) {
showError(getErrorMessage(gitIgnoreError || licenseError || systemConfigError), 0) showError(
getErrorMessage(gitIgnoreError || licenseError || systemConfigError || errorWhileFetchingAuthSettings),
0
)
} }
}, [gitIgnoreError, licenseError, systemConfigError, showError]) }, [gitIgnoreError, licenseError, systemConfigError, errorWhileFetchingAuthSettings, showError])
useEffect(() => {
if (systemConfig) {
setEnablePublicRepo(systemConfig.public_resource_creation_enabled)
}
}, [systemConfig])
const handleSubmit = (formData: RepoFormData) => { const handleSubmit = (formData: RepoFormData) => {
try { try {
const payload: OpenapiCreateRepositoryRequest = { const payload: OpenapiCreateRepositoryRequest = {
@ -354,7 +344,7 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
{getString('createRepoModal.branch')} {getString('createRepoModal.branch')}
</Text> </Text>
</Container> </Container>
<Render when={enablePublicRepo}> <Render when={allowPublicResourceCreation}>
<hr className={css.dividerContainer} /> <hr className={css.dividerContainer} />
<Container> <Container>
<FormInput.RadioGroup <FormInput.RadioGroup

View File

@ -651,8 +651,11 @@ export interface StringsMap {
'labels.createdIn': string 'labels.createdIn': string
'labels.deleteLabel': string 'labels.deleteLabel': string
'labels.deleteLabelConfirm': string 'labels.deleteLabelConfirm': string
'labels.deletedLabel': string
'labels.descriptionOptional': string 'labels.descriptionOptional': string
'labels.failedToDeleteLabel': string 'labels.failedToDeleteLabel': string
'labels.failedtoFetchLabels': string
'labels.failedtoFetchValues': string
'labels.filterByLabels': string 'labels.filterByLabels': string
'labels.findALabel': string 'labels.findALabel': string
'labels.findOrAdd': string 'labels.findOrAdd': string

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
export function useGetAuthSettings<T = Record<string, boolean>>() {
return {} as T
}

View File

@ -0,0 +1,51 @@
/*
* 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 { useEffect, useMemo } from 'react'
import { useGet } from 'restful-react'
import { useAppContext } from 'AppContext'
export function usePublicResourceConfig() {
const { standalone, hooks } = useAppContext()
const { refetchAuthSettings, authSettings, fetchingAuthSettings, errorWhileFetchingAuthSettings } =
hooks.useGetAuthSettings()
const {
data: systemConfig,
loading: systemConfigLoading,
error: systemConfigError
} = useGet({ path: 'api/v1/system/config' })
useEffect(() => {
if (!standalone) refetchAuthSettings()
}, [refetchAuthSettings])
const allowPublicResourceCreation = useMemo(() => {
if (systemConfigLoading || fetchingAuthSettings) {
return false
}
if (standalone) {
return systemConfig?.public_resource_creation_enabled
}
return !!(systemConfig?.public_resource_creation_enabled && authSettings?.resource?.publicAccessEnabled)
}, [authSettings, systemConfig, standalone, systemConfigLoading, fetchingAuthSettings])
return {
allowPublicResourceCreation,
configLoading: fetchingAuthSettings || systemConfigLoading,
systemConfigError,
errorWhileFetchingAuthSettings
}
}

View File

@ -128,9 +128,9 @@ repos:
createRepoModal: createRepoModal:
branchLabel: 'Your repository will be initialized with a ' branchLabel: 'Your repository will be initialized with a '
branch: ' branch.' branch: ' branch.'
publicLabel: Anyone with access to the Gitness environment can clone this repo. publicLabel: Anyone who can view the repository can clone it.
privateLabel: You choose who can see and commit to this repository. privateLabel: You choose who can see and commit to this repository.
publicWarning: Please note that anyone with access to the Gitness environment can clone this repo. publicWarning: Please note that anyone who can view the repository can clone it.
validation: validation:
repoNamePatternIsNotValid: Registry name should be 1~255 characters long with lower case characters, numbers and ._- and must be start with numbers or characters repoNamePatternIsNotValid: Registry name should be 1~255 characters long with lower case characters, numbers and ._- and must be start with numbers or characters
gitBranchNameInvalid: Branch name is invalid. gitBranchNameInvalid: Branch name is invalid.
@ -1315,6 +1315,7 @@ labels:
addValue: 'Add value' addValue: 'Add value'
removeLabel: Remove Label removeLabel: Remove Label
deleteLabel: Delete Label deleteLabel: Delete Label
deletedLabel: 'Deleted Label : {tag}'
provideLabelName: Provide Label name provideLabelName: Provide Label name
labelCreated: Label created labelCreated: Label created
labelUpdated: Label updated labelUpdated: Label updated
@ -1347,6 +1348,8 @@ labels:
deleteLabelConfirm: Are you sure you want to delete label <strong>{{name}}</strong>? You can't undo this action. deleteLabelConfirm: Are you sure you want to delete label <strong>{{name}}</strong>? You can't undo this action.
intentText: Editing/deleting a label or its values will impact all the areas it has been used. intentText: Editing/deleting a label or its values will impact all the areas it has been used.
prCount: 'Showing {count} {count|1:result,results}' prCount: 'Showing {count} {count|1:result,results}'
failedtoFetchLabels: Failed to fetch labels
failedtoFetchValues: Failed to fetch label values
noResults: No results found noResults: No results found
labelPreview: Label Preview labelPreview: Label Preview
filterByLabels: Filter by Label/s filterByLabels: Filter by Label/s

View File

@ -33,7 +33,7 @@ import {
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system' import { Color, FontVariation } from '@harnessio/design-system'
import { Menu, MenuItem, PopoverInteractionKind, Position } from '@blueprintjs/core' import { Menu, MenuItem, PopoverInteractionKind, Position, Spinner } from '@blueprintjs/core'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FieldArray } from 'formik' import { FieldArray } from 'formik'
import { useGet, useMutate } from 'restful-react' import { useGet, useMutate } from 'restful-react'
@ -181,30 +181,51 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
path: updateLabel?.scope ? `/spaces/${scopeRef as string}/+/labels` : `/spaces/${space as string}/+/labels` path: updateLabel?.scope ? `/spaces/${scopeRef as string}/+/labels` : `/spaces/${space as string}/+/labels`
}) })
const getPath = () =>
updateLabel?.scope === 0 && repoMetadata
? `/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(
updateLabel?.key ? updateLabel?.key : ''
)}/values`
: `/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(
updateLabel?.key ? updateLabel?.key : ''
)}/values`
const { const {
data: repoLabelValues, data: initLabelValues,
loading: repoValueListLoading, loading: initValueListLoading,
refetch: refetchRepoValuesList refetch: refetchInitValuesList
} = useGet<TypesLabelValue[]>({ } = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'), base: getConfig('code/api/v1'),
path: `/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent( path: getPath(),
updateLabel?.key ? updateLabel?.key : ''
)}/values`,
lazy: true lazy: true
}) })
//ToDo : Remove getLabelValuesPromiseQuery component when Encoding is handled by BE for Harness
const getPathHarness = () =>
updateLabel?.scope === 0 && repoMetadata
? `/repos/${repoMetadata?.identifier}/labels/${encodeURIComponent(
updateLabel?.key ? updateLabel?.key : ''
)}/values`
: `/labels/${encodeURIComponent(updateLabel?.key ? updateLabel?.key : '')}/values`
const { const {
data: spaceLabelValues, data: initLabelValuesQuery,
loading: spaceValueListLoading, loading: initValueListLoadingQuery,
refetch: refetchSpaceValuesList refetch: refetchInitValuesListQuery
} = useGet<TypesLabelValue[]>({ } = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'), base: getConfig('code/api/v1'),
path: `/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent( path: getPathHarness(),
updateLabel?.key ? updateLabel?.key : '' queryParams: {
)}/values`, accountIdentifier: scopeRef?.split('/')[0],
orgIdentifier: scopeRef?.split('/')[1],
projectIdentifier: scopeRef?.split('/')[2]
},
lazy: true lazy: true
}) })
//ToDo: Remove all references of suffix with Query when Encoding is handled by BE for Harness
const [openModal, hideModal] = useModalHook(() => { const [openModal, hideModal] = useModalHook(() => {
const handleLabelSubmit = (formData: LabelFormData) => { const handleLabelSubmit = (formData: LabelFormData) => {
const { labelName, color, labelValues, description, allowDynamicValues, id } = formData const { labelName, color, labelValues, description, allowDynamicValues, id } = formData
@ -329,20 +350,20 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
return { ...baseValues, color: ColorName.Blue } return { ...baseValues, color: ColorName.Blue }
} }
if (repoMetadata && updateLabel?.scope === 0) { if (modalMode === ModalMode.UPDATE && updateLabel?.value_count === 0) return baseValues
return { ...baseValues, labelValues: getLabelValues(repoLabelValues ?? undefined) }
}
return { ...baseValues, labelValues: getLabelValues(spaceLabelValues ?? undefined) } if (standalone) {
return { ...baseValues, labelValues: getLabelValues(initLabelValues ?? undefined) }
}
return { ...baseValues, labelValues: getLabelValues(initLabelValuesQuery ?? undefined) }
})() })()
return ( return (
<Dialog <Dialog
isOpen isOpen
onOpening={() => { onOpening={() => {
if (modalMode === ModalMode.UPDATE) { if (modalMode === ModalMode.UPDATE && updateLabel?.value_count !== 0) {
if (repoMetadata && updateLabel?.scope === 0) refetchRepoValuesList() standalone ? refetchInitValuesList() : refetchInitValuesListQuery()
else refetchSpaceValuesList()
} }
}} }}
enforceFocus={false} enforceFocus={false}
@ -408,6 +429,13 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
<FieldArray <FieldArray
name="labelValues" name="labelValues"
render={({ push, remove }) => { render={({ push, remove }) => {
if (
modalMode === ModalMode.UPDATE &&
updateLabel?.value_count !== 0 &&
(initValueListLoading || initValueListLoadingQuery)
)
return <Spinner size={20} />
else
return ( return (
<Layout.Vertical> <Layout.Vertical>
{formik.values.labelValues?.map((_, index) => ( {formik.values.labelValues?.map((_, index) => (
@ -498,6 +526,11 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
<Layout.Vertical <Layout.Vertical
style={{ width: '45%', padding: '25px 35px 25px 35px', borderLeft: '1px solid var(--grey-100)' }}> style={{ width: '45%', padding: '25px 35px 25px 35px', borderLeft: '1px solid var(--grey-100)' }}>
<Text>{getString('labels.labelPreview')}</Text> <Text>{getString('labels.labelPreview')}</Text>
{modalMode === ModalMode.UPDATE &&
updateLabel?.value_count !== 0 &&
(initValueListLoading || initValueListLoadingQuery) ? (
<Spinner size={20} />
) : (
<Layout.Vertical spacing={'medium'}> <Layout.Vertical spacing={'medium'}>
{formik.values.labelValues?.length {formik.values.labelValues?.length
? formik.values.labelValues?.map((valueObj, i) => ( ? formik.values.labelValues?.map((valueObj, i) => (
@ -529,6 +562,7 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
/> />
)} )}
</Layout.Vertical> </Layout.Vertical>
)}
</Layout.Vertical> </Layout.Vertical>
</Layout.Horizontal> </Layout.Horizontal>
</FormikForm> </FormikForm>
@ -539,11 +573,12 @@ const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
) )
}, [ }, [
updateLabel, updateLabel,
repoLabelValues, initLabelValues,
spaceLabelValues, initLabelValuesQuery,
repoValueListLoading, initValueListLoading,
spaceValueListLoading, initValueListLoadingQuery,
refetchlabelsList, refetchlabelsList,
refetchInitValuesListQuery,
modalMode modalMode
]) ])

View File

@ -16,16 +16,7 @@
.main { .main {
background-color: var(--primary-bg) !important; background-color: var(--primary-bg) !important;
.table {
.row {
height: 80px;
display: flex;
justify-content: center;
.title {
font-weight: 600;
}
}
}
.noData > div { .noData > div {
height: calc(100vh - var(--page-header-height, 64px) - 120px) !important; height: calc(100vh - var(--page-header-height, 64px) - 120px) !important;
} }

View File

@ -19,10 +19,7 @@
export declare const cancelButton: string 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 scopeCheckbox: string export declare const scopeCheckbox: string
export declare const table: string
export declare const title: string
export declare const toggle: string export declare const toggle: string
export declare const toggleDisable: string export declare const toggleDisable: string
export declare const toggleEnable: string export declare const toggleEnable: string

View File

@ -18,9 +18,8 @@ import { Container, Layout, FlexExpander, ButtonVariation, Button, Checkbox } fr
import { Render } from 'react-jsx-match' import { Render } from 'react-jsx-match'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { CodeIcon } from 'utils/GitUtils' import { CodeIcon } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { LabelsPageScope, permissionProps } from 'utils/Utils' import type { LabelsPageScope } from 'utils/Utils'
import type { RepoRepositoryOutput } from 'services/code' import type { RepoRepositoryOutput } from 'services/code'
import css from './LabelsHeader.module.scss' import css from './LabelsHeader.module.scss'
@ -30,28 +29,10 @@ const LabelsHeader = ({
showParentScopeFilter, showParentScopeFilter,
inheritLabels, inheritLabels,
setInheritLabels, setInheritLabels,
openLabelCreateModal, openLabelCreateModal
spaceRef,
repoMetadata,
currentPageScope
}: LabelsHeaderProps) => { }: LabelsHeaderProps) => {
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const { getString } = useStrings() const { getString } = useStrings()
const { hooks, standalone } = useAppContext()
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier:
currentPageScope === LabelsPageScope.REPOSITORY && repoMetadata
? repoMetadata.identifier
: (spaceRef as string)
},
permissions: ['code_repo_edit']
},
[spaceRef]
)
//ToDo: check space permissions as well in case of spaces //ToDo: check space permissions as well in case of spaces
@ -63,7 +44,6 @@ const LabelsHeader = ({
text={getString('labels.newLabel')} text={getString('labels.newLabel')}
icon={CodeIcon.Add} icon={CodeIcon.Add}
onClick={openLabelCreateModal} onClick={openLabelCreateModal}
{...permissionProps(permPushResult, standalone)}
/> />
<Render when={showParentScopeFilter}> <Render when={showParentScopeFilter}>
<Checkbox <Checkbox

View File

@ -29,60 +29,16 @@
max-width: 55%; max-width: 55%;
} }
div[class*='TableV2--row'] {
padding: 3px var(--spacing-medium);
justify-content: center;
min-height: 48px;
}
div[class*='TableV2--rowSubComponent'] { div[class*='TableV2--rowSubComponent'] {
border-top: 1px solid var(--grey-100); border-top: 1px solid var(--grey-100);
padding: 20px 4px 4px 5%; padding: 14px var(--spacing-xsmall) 14px 4.7%;
} margin-top: var(--spacing-tiny);
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 10px 0;
width: 95%;
}
.border {
border-bottom: 1px solid var(--grey-100) !important;
}
.hideDetailsContainer {
display: none;
}
.appliedRulesTextContainer {
border-radius: 4px;
background: var(--grey-50) !important;
font-size: 12px !important;
font-weight: 500 !important;
padding: var(--spacing-small) !important;
margin-bottom: var(--spacing-xsmall) !important;
}
.popover {
z-index: 999;
padding: var(--spacing-tiny) !important;
}
.widthContainer {
max-width: calc(100% - 100px);
overflow-x: hidden;
display: flex;
flex-wrap: wrap;
}
.hideButtonIcon {
:global {
[class*='ConfirmationDialog--header'] {
.bp3-icon {
display: none !important;
}
}
[class*='ConfirmationDialog--body'] {
padding-left: 3px !important;
}
} }
} }
} }

View File

@ -16,16 +16,9 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const appliedRulesTextContainer: string
export declare const border: string
export declare const dividerContainer: string
export declare const hideButtonIcon: string
export declare const hideDetailsContainer: string
export declare const labelCtn: string export declare const labelCtn: string
export declare const main: string export declare const main: string
export declare const optionItem: string export declare const optionItem: string
export declare const popover: string
export declare const row: string export declare const row: string
export declare const table: string export declare const table: string
export declare const toggleAccordion: string export declare const toggleAccordion: string
export declare const widthContainer: string

View File

@ -38,7 +38,6 @@ import { usePageIndex } from 'hooks/usePageIndex'
import { import {
getErrorMessage, getErrorMessage,
LIST_FETCHING_LIMIT, LIST_FETCHING_LIMIT,
permissionProps,
type PageBrowserProps, type PageBrowserProps,
ColorName, ColorName,
LabelTypes, LabelTypes,
@ -54,7 +53,7 @@ import { useConfirmAction } from 'hooks/useConfirmAction'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams' import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { LabelTitle, LabelValuesList } from 'components/Label/Label' import { LabelTitle, LabelValuesList, LabelValuesListQuery } from 'components/Label/Label'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { getConfig } from 'services/config' import { getConfig } from 'services/config'
import LabelsHeader from './LabelsHeader/LabelsHeader' import LabelsHeader from './LabelsHeader/LabelsHeader'
@ -63,7 +62,7 @@ import css from './LabelsListing.module.scss'
const LabelsListing = (props: LabelListingProps) => { const LabelsListing = (props: LabelListingProps) => {
const { activeTab, currentPageScope, repoMetadata, space } = props const { activeTab, currentPageScope, repoMetadata, space } = props
const { hooks, standalone } = useAppContext() const { standalone } = useAppContext()
const { getString } = useStrings() const { getString } = useStrings()
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const history = useHistory() const history = useHistory()
@ -133,6 +132,7 @@ const LabelsListing = (props: LabelListingProps) => {
const { openModal: openLabelCreateModal, openUpdateLabelModal } = useLabelModal({ refetchlabelsList }) const { openModal: openLabelCreateModal, openUpdateLabelModal } = useLabelModal({ refetchlabelsList })
const renderRowSubComponent = React.useCallback(({ row }: { row: Row<LabelTypes> }) => { const renderRowSubComponent = React.useCallback(({ row }: { row: Row<LabelTypes> }) => {
if (standalone) {
return ( return (
<LabelValuesList <LabelValuesList
name={row.original?.key as string} name={row.original?.key as string}
@ -142,6 +142,17 @@ const LabelsListing = (props: LabelListingProps) => {
standalone={standalone} standalone={standalone}
/> />
) )
} else {
return (
<LabelValuesListQuery
name={row.original?.key as string}
scope={row.original?.scope as number}
repoMetadata={repoMetadata}
space={space}
standalone={standalone}
/>
)
}
}, []) }, [])
const ToggleAccordionCell: Renderer<{ const ToggleAccordionCell: Renderer<{
@ -215,7 +226,7 @@ const LabelsListing = (props: LabelListingProps) => {
width: '40%', width: '40%',
sort: 'true', sort: 'true',
Cell: ({ row }: CellProps<LabelTypes>) => { Cell: ({ row }: CellProps<LabelTypes>) => {
return <Text lineClamp={3}>{row.original?.description}</Text> return <Text lineClamp={1}>{row.original?.description}</Text>
} }
}, },
{ {
@ -235,6 +246,24 @@ const LabelsListing = (props: LabelListingProps) => {
path: deleteLabelPath path: deleteLabelPath
}) })
//ToDo : Remove the following block when Encoding is handled by BE for Harness
const deleteLabelPathHarness =
row.original?.scope === 0
? `/repos/${repoMetadata?.identifier}/labels/${encodedLabelKey}`
: `/labels/${encodedLabelKey}`
const { mutate: deleteLabelQueryCall } = useMutate({
verb: 'DELETE',
base: getConfig('code/api/v1'),
path: deleteLabelPathHarness,
queryParams: {
accountIdentifier: scopeRef?.split('/')[0],
orgIdentifier: scopeRef?.split('/')[1],
projectIdentifier: scopeRef?.split('/')[2]
}
})
//ToDo: remove type check of standalone when Encoding is handled by BE for Harness
const confirmLabelDelete = useConfirmAction({ const confirmLabelDelete = useConfirmAction({
title: getString('labels.deleteLabel'), title: getString('labels.deleteLabel'),
confirmText: getString('delete'), confirmText: getString('delete'),
@ -242,23 +271,19 @@ const LabelsListing = (props: LabelListingProps) => {
message: <String useRichText stringID="labels.deleteLabelConfirm" vars={{ name: row.original.key }} />, message: <String useRichText stringID="labels.deleteLabelConfirm" vars={{ name: row.original.key }} />,
action: async e => { action: async e => {
e.stopPropagation() e.stopPropagation()
deleteLabel({}) const handleSuccess = (tag: string) => {
.then(() => { showSuccess(<StringSubstitute str={getString('labels.deletedLabel')} vars={{ tag }} />, 5000)
showSuccess(
<StringSubstitute
str={getString('labels.deleteLabel')}
vars={{
tag: row.original.key
}}
/>,
5000
)
refetchlabelsList() refetchlabelsList()
setPage(1) setPage(1)
}) }
.catch(error => {
const handleError = (error: any) => {
showError(getErrorMessage(error), 0, getString('labels.failedToDeleteLabel')) showError(getErrorMessage(error), 0, getString('labels.failedToDeleteLabel'))
}) }
const deleteAction = standalone ? deleteLabel({}) : deleteLabelQueryCall({})
deleteAction.then(() => handleSuccess(row.original.key ?? '')).catch(handleError)
} }
}) })
return ( return (
@ -296,18 +321,6 @@ const LabelsListing = (props: LabelListingProps) => {
[history, getString, repoMetadata?.path, space, setPage, showError, showSuccess] [history, getString, repoMetadata?.path, space, setPage, showError, showSuccess]
) )
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier:
currentPageScope === LabelsPageScope.REPOSITORY ? (repoMetadata?.identifier as string) : (space as string)
},
permissions: ['code_repo_edit']
},
[space]
)
return ( return (
<Container> <Container>
<LabelsHeader <LabelsHeader
@ -346,7 +359,6 @@ const LabelsListing = (props: LabelListingProps) => {
message={getString('labels.noLabelsFound')} message={getString('labels.noLabelsFound')}
buttonText={getString('labels.newLabel')} buttonText={getString('labels.newLabel')}
onButtonClick={() => openLabelCreateModal()} onButtonClick={() => openLabelCreateModal()}
permissionProp={permissionProps(permPushResult, standalone)}
/> />
</Container> </Container>
) )

View File

@ -51,6 +51,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0px 0.5px 2px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.08); box-shadow: 0px 0.5px 2px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.08);
background: var(--grey-0); background: var(--grey-0);
cursor: pointer;
&.expanded { &.expanded {
.chevron { .chevron {

View File

@ -81,6 +81,8 @@ export interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | '
standalone: boolean standalone: boolean
routingId: string routingId: string
pullReqCommits: TypesListCommitResponse | undefined pullReqCommits: TypesListCommitResponse | undefined
refetchActivities: () => void
refetchPullReq: () => void
} }
export const Conversation: React.FC<ConversationProps> = ({ export const Conversation: React.FC<ConversationProps> = ({
@ -93,7 +95,9 @@ export const Conversation: React.FC<ConversationProps> = ({
prChecksDecisionResult, prChecksDecisionResult,
standalone, standalone,
routingId, routingId,
pullReqCommits pullReqCommits,
refetchActivities,
refetchPullReq
}) => { }) => {
const { getString } = useStrings() const { getString } = useStrings()
const { currentUser, routes, hooks } = useAppContext() const { currentUser, routes, hooks } = useAppContext()
@ -442,6 +446,7 @@ export const Conversation: React.FC<ConversationProps> = ({
setActivityFilter={setActivityFilter} setActivityFilter={setActivityFilter}
loadingReviewers={loadingReviewers} loadingReviewers={loadingReviewers}
refetchCodeOwners={refetchCodeOwners} refetchCodeOwners={refetchCodeOwners}
refetchPullReq={refetchPullReq}
activities={activities} activities={activities}
/> />
</Container> </Container>
@ -508,6 +513,7 @@ export const Conversation: React.FC<ConversationProps> = ({
refetchReviewers={refetchReviewers} refetchReviewers={refetchReviewers}
labels={labels} labels={labels}
refetchLabels={refetchLabels} refetchLabels={refetchLabels}
refetchActivities={refetchActivities}
/> />
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>

View File

@ -35,7 +35,11 @@ import { useAppContext } from 'AppContext'
import type { ConversationProps } from './Conversation' import type { ConversationProps } from './Conversation'
import css from './Conversation.module.scss' import css from './Conversation.module.scss'
interface DescriptionBoxProps extends Omit<ConversationProps, 'onCancelEditDescription' | 'pullReqCommits'> { interface DescriptionBoxProps
extends Omit<
ConversationProps,
'onCancelEditDescription' | 'pullReqCommits' | 'refetchActivities' | 'refetchPullReq'
> {
onCancelEditDescription: () => void onCancelEditDescription: () => void
pullReqCommits: TypesListCommitResponse | undefined pullReqCommits: TypesListCommitResponse | undefined
} }

View File

@ -65,7 +65,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
allowedStrategy, allowedStrategy,
pullReqCommits, pullReqCommits,
PRStateLoading, PRStateLoading,
setConflictingFiles setConflictingFiles,
refetchPullReq
}) => { }) => {
const { getString } = useStrings() const { getString } = useStrings()
const { showError } = useToaster() const { showError } = useToaster()
@ -127,7 +128,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
setAllowedStrats, setAllowedStrats,
pullRequestSection, pullRequestSection,
showError, showError,
setConflictingFiles setConflictingFiles,
refetchPullReq
) // eslint-disable-next-line react-hooks/exhaustive-deps ) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [unchecked, pullReqMetadata?.source_sha]) }, [unchecked, pullReqMetadata?.source_sha])
const [prMerged, setPrMerged] = useState(false) const [prMerged, setPrMerged] = useState(false)
@ -146,7 +148,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
setAllowedStrats, setAllowedStrats,
pullRequestSection, pullRequestSection,
showError, showError,
setConflictingFiles setConflictingFiles,
refetchPullReq
) )
} }
}, POLLING_INTERVAL) // Poll every 20 seconds }, POLLING_INTERVAL) // Poll every 20 seconds
@ -154,7 +157,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
return () => { return () => {
clearInterval(intervalId) clearInterval(intervalId)
} // eslint-disable-next-line react-hooks/exhaustive-deps } // eslint-disable-next-line react-hooks/exhaustive-deps
}, [onPRStateChanged, prMerged]) }, [onPRStateChanged, prMerged, pullReqMetadata?.source_sha])
const isDraft = pullReqMetadata.is_draft const isDraft = pullReqMetadata.is_draft
const mergeOptions: PRMergeOption[] = [ const mergeOptions: PRMergeOption[] = [
{ {

View File

@ -51,6 +51,7 @@ interface PullRequestOverviewPanelProps {
setActivityFilter: (val: SelectOption) => void setActivityFilter: (val: SelectOption) => void
loadingReviewers: boolean loadingReviewers: boolean
refetchCodeOwners: () => void refetchCodeOwners: () => void
refetchPullReq: () => void
activities: TypesPullReqActivity[] | undefined activities: TypesPullReqActivity[] | undefined
pullReqCommits: TypesListCommitResponse | undefined pullReqCommits: TypesListCommitResponse | undefined
} }
@ -67,7 +68,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
loadingReviewers, loadingReviewers,
refetchCodeOwners, refetchCodeOwners,
activities, activities,
pullReqCommits pullReqCommits,
refetchPullReq
} = props } = props
const { getString } = useStrings() const { getString } = useStrings()
const { showError } = useToaster() const { showError } = useToaster()
@ -174,6 +176,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
pullRequestSection, pullRequestSection,
showError, showError,
setConflictingFiles, setConflictingFiles,
refetchPullReq,
setRequiresCommentApproval, setRequiresCommentApproval,
setAtLeastOneReviewerRule, setAtLeastOneReviewerRule,
setReqCodeOwnerApproval, setReqCodeOwnerApproval,
@ -197,6 +200,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
allowedStrategy={allowedStrats} allowedStrategy={allowedStrats}
pullReqCommits={pullReqCommits} pullReqCommits={pullReqCommits}
PRStateLoading={PRStateLoading || loadingReviewers} PRStateLoading={PRStateLoading || loadingReviewers}
refetchPullReq={refetchPullReq}
/> />
{pullReqMetadata.state !== PullRequestState.CLOSED && ( {pullReqMetadata.state !== PullRequestState.CLOSED && (
<PullRequestPanelSections <PullRequestPanelSections

View File

@ -132,7 +132,7 @@ const MergeSection = (props: MergeSectionProps) => {
</Layout.Vertical> </Layout.Vertical>
)} )}
</Layout.Horizontal> </Layout.Horizontal>
{!mergeable && ( {!mergeable && !unchecked && (
<Button <Button
padding={{ right: 'unset' }} padding={{ right: 'unset' }}
className={cx(css.blueText, css.buttonPadding)} className={cx(css.blueText, css.buttonPadding)}

View File

@ -15,7 +15,7 @@
*/ */
import React, { useState } from 'react' import React, { useState } from 'react'
import { PopoverInteractionKind } from '@blueprintjs/core' import { PopoverInteractionKind, Spinner } from '@blueprintjs/core'
import { useGet, useMutate } from 'restful-react' import { useGet, useMutate } from 'restful-react'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import cx from 'classnames' import cx from 'classnames'
@ -43,13 +43,15 @@ interface PullRequestSideBarProps {
pullRequestMetadata: TypesPullReq pullRequestMetadata: TypesPullReq
refetchReviewers: () => void refetchReviewers: () => void
refetchLabels: () => void refetchLabels: () => void
refetchActivities: () => void
} }
const PullRequestSideBar = (props: PullRequestSideBarProps) => { const PullRequestSideBar = (props: PullRequestSideBarProps) => {
const { standalone, hooks } = useAppContext() const { standalone, hooks } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags() const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const [labelQuery, setLabelQuery] = useState<string>('') const [labelQuery, setLabelQuery] = useState<string>('')
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers, labels, refetchLabels } = props const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers, labels, refetchLabels, refetchActivities } =
props
const { getString } = useStrings() const { getString } = useStrings()
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const generateReviewDecisionInfo = ( const generateReviewDecisionInfo = (
@ -166,13 +168,10 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}` path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}`
}) })
const { mutate: removeLabel } = useMutate({ const { mutate: removeLabel, loading: removingLabel } = useMutate({
verb: 'DELETE', verb: 'DELETE',
base: getConfig('code/api/v1'), base: getConfig('code/api/v1'),
path: ({ label_id }) => path: ({ label_id }) => `/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/labels/${label_id}`
`/repos/${encodeURIComponent(repoMetadata.path as string)}/pullreq/${
pullRequestMetadata?.number
}/labels/${encodeURIComponent(label_id)}`
}) })
const { const {
@ -420,6 +419,7 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
query={labelQuery} query={labelQuery}
setQuery={setLabelQuery} setQuery={setLabelQuery}
labelListLoading={labelListLoading} labelListLoading={labelListLoading}
refetchActivities={refetchActivities}
/> />
</Layout.Horizontal> </Layout.Horizontal>
<Container padding={{ top: 'medium', bottom: 'large' }}> <Container padding={{ top: 'medium', bottom: 'large' }}>
@ -451,6 +451,7 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
label: label.key label: label.key
}) as string }) as string
) )
refetchActivities()
}) })
.catch(err => { .catch(err => {
showError(getErrorMessage(err)) showError(getErrorMessage(err))
@ -464,6 +465,7 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
{getString('labels.noLabels')} {getString('labels.noLabels')}
</Text> </Text>
)} )}
{removingLabel && <Spinner size={16} />}
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
</Layout.Vertical> </Layout.Vertical>

View File

@ -59,6 +59,7 @@ export default function PullRequest() {
commitSHA, commitSHA,
refetchActivities, refetchActivities,
refetchCommits, refetchCommits,
refetchPullReq,
retryOnErrorFunc retryOnErrorFunc
} = useGetPullRequestInfo() } = useGetPullRequestInfo()
@ -158,6 +159,8 @@ export default function PullRequest() {
prStats={pullReqStats} prStats={pullReqStats}
showEditDescription={showEditDescription} showEditDescription={showEditDescription}
onCancelEditDescription={() => setShowEditDescription(false)} onCancelEditDescription={() => setShowEditDescription(false)}
refetchPullReq={refetchPullReq}
refetchActivities={refetchActivities}
/> />
) )
}, },

View File

@ -207,6 +207,7 @@ export function useGetPullRequestInfo() {
commitSHA, commitSHA,
refetchActivities, refetchActivities,
refetchCommits, refetchCommits,
refetchPullReq,
retryOnErrorFunc retryOnErrorFunc
} }
} }

View File

@ -274,8 +274,7 @@ export default function PullRequests() {
(isLabelEnabled || standalone) && (isLabelEnabled || standalone) &&
row.original && row.original &&
row.original.labels && row.original.labels &&
row.original.labels.length !== 0 && row.original.labels.length !== 0
!prLoading
}> }>
{row.original?.labels?.map((label, index) => ( {row.original?.labels?.map((label, index) => (
<Label <Label
@ -406,19 +405,14 @@ export default function PullRequests() {
<Layout.Horizontal <Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
style={{ flexWrap: 'wrap', gap: '5px' }}> style={{ flexWrap: 'wrap', gap: '5px' }}>
<Render when={!prLoading}>
{isEmpty(data) ? (
<Text color={Color.GREY_400}>{getString('labels.noResults')}</Text>
) : (
<Text color={Color.GREY_400}> <Text color={Color.GREY_400}>
{ {isEmpty(data)
stringSubstitute(getString('labels.prCount'), { ? getString('labels.noResults')
: (stringSubstitute(getString('labels.prCount'), {
count: data?.length count: data?.length
}) as string }) as string)}
}
</Text> </Text>
)}
</Render>
{(isLabelEnabled || standalone) && {(isLabelEnabled || standalone) &&
labelFilter && labelFilter &&
labelFilter?.length !== 0 && labelFilter?.length !== 0 &&

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react' import React, { useState } from 'react'
import { import {
Container, Container,
Layout, Layout,
@ -32,7 +32,7 @@ import cx from 'classnames'
import { Color, FontVariation, Intent } from '@harnessio/design-system' import { Color, FontVariation, Intent } from '@harnessio/design-system'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useMutate, useGet } from 'restful-react' import { useMutate } from 'restful-react'
import { Render } from 'react-jsx-match' import { Render } from 'react-jsx-match'
import { ACCESS_MODES, getErrorMessage, permissionProps, voidFn } from 'utils/Utils' import { ACCESS_MODES, getErrorMessage, permissionProps, voidFn } from 'utils/Utils'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
@ -42,6 +42,7 @@ import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { RepoVisibility } from 'utils/GitUtils' import { RepoVisibility } from 'utils/GitUtils'
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect' import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
import { useModalHook } from 'hooks/useModalHook' import { useModalHook } from 'hooks/useModalHook'
import { usePublicResourceConfig } from 'hooks/usePublicResourceConfig'
import useDeleteRepoModal from './DeleteRepoModal/DeleteRepoModal' import useDeleteRepoModal from './DeleteRepoModal/DeleteRepoModal'
import useDefaultBranchModal from './DefaultBranchModal/DefaultBranchModal' import useDefaultBranchModal from './DefaultBranchModal/DefaultBranchModal'
import Private from '../../../icons/private.svg?url' import Private from '../../../icons/private.svg?url'
@ -62,14 +63,13 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
const [defaultBranch, setDefaultBranch] = useState(ACCESS_MODES.VIEW) const [defaultBranch, setDefaultBranch] = useState(ACCESS_MODES.VIEW)
const { openModal: openDefaultBranchModal } = useDefaultBranchModal({ currentGitRef, setDefaultBranch, refetch }) const { openModal: openDefaultBranchModal } = useDefaultBranchModal({ currentGitRef, setDefaultBranch, refetch })
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const { standalone, hooks } = useAppContext()
const space = useGetSpaceParam() const space = useGetSpaceParam()
const { standalone, hooks, isPublicAccessEnabledOnResources } = useAppContext() const { allowPublicResourceCreation } = usePublicResourceConfig()
const { getString } = useStrings() const { getString } = useStrings()
const currRepoVisibility = repoMetadata?.is_public === true ? RepoVisibility.PUBLIC : RepoVisibility.PRIVATE const currRepoVisibility = repoMetadata?.is_public === true ? RepoVisibility.PUBLIC : RepoVisibility.PRIVATE
const [repoVis, setRepoVis] = useState<RepoVisibility>(currRepoVisibility) const [repoVis, setRepoVis] = useState<RepoVisibility>(currRepoVisibility)
const [enablePublicRepo, setEnablePublicRepo] = useState(false)
const { mutate } = useMutate({ const { mutate } = useMutate({
verb: 'PATCH', verb: 'PATCH',
path: `/api/v1/repos/${repoMetadata?.path}/+/` path: `/api/v1/repos/${repoMetadata?.path}/+/`
@ -100,19 +100,11 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
}, },
[space] [space]
) )
const { data: systemConfig } = useGet({ path: 'api/v1/system/config' })
useEffect(() => {
if (systemConfig) {
setEnablePublicRepo(systemConfig.public_resource_creation_enabled)
}
}, [systemConfig])
const ModalComponent: React.FC = () => { const ModalComponent: React.FC = () => {
return ( return (
<Dialog <Dialog
className={css.dialogContainer} className={css.dialogContainer}
style={{ width: 585, maxHeight: '95vh', overflow: 'auto' }}
title={<Text font={{ variation: FontVariation.H4 }}>{getString('changeRepoVis')}</Text>} title={<Text font={{ variation: FontVariation.H4 }}>{getString('changeRepoVis')}</Text>}
isOpen isOpen
onClose={hideModal}> onClose={hideModal}>
@ -144,7 +136,6 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
</Container> </Container>
<Layout.Horizontal className={css.buttonContainer}> <Layout.Horizontal className={css.buttonContainer}>
<Button <Button
margin={{ right: 'medium' }}
type="submit" type="submit"
text={ text={
<StringSubstitute <StringSubstitute
@ -333,7 +324,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
</Container> </Container>
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
<Render when={enablePublicRepo && isPublicAccessEnabledOnResources}> <Render when={allowPublicResourceCreation}>
<Container padding="large" margin={{ bottom: 'medium' }} className={css.generalContainer}> <Container padding="large" margin={{ bottom: 'medium' }} className={css.generalContainer}>
<Layout.Horizontal padding={{ bottom: 'medium' }}> <Layout.Horizontal padding={{ bottom: 'medium' }}>
<Container className={css.label}> <Container className={css.label}>

View File

@ -121,6 +121,9 @@
} }
.dialogContainer { .dialogContainer {
width: 600px;
max-height: 95vh;
overflow: auto;
:global(.bp3-dialog-header) { :global(.bp3-dialog-header) {
margin-bottom: var(--spacing-medium) !important; margin-bottom: var(--spacing-medium) !important;
} }
@ -144,7 +147,7 @@
.buttonContainer { .buttonContainer {
width: 20%; width: 20%;
padding-top: var(--spacing-xsmall) !important; padding-top: var(--spacing-xsmall) !important;
gap: 10px; gap: 15px;
margin-left: auto !important; margin-left: auto !important;
} }

View File

@ -85,6 +85,12 @@ export const getUsingFetch = <
if (res.status === 401) { if (res.status === 401) {
return res.json().then(json => Promise.reject(json)) return res.json().then(json => Promise.reject(json))
} }
if (res.status === 400) {
return res.text().then(text => Promise.reject(text))
}
if (res.status === 403) {
return res.text().then(text => Promise.reject(text))
}
return res.json() return res.json()
} }
@ -100,6 +106,14 @@ export const getUsingFetch = <
return res.text().then(text => Promise.reject(text)) return res.text().then(text => Promise.reject(text))
} }
if (res.status === 403) {
return res.text().then(text => Promise.reject(text))
}
if (res.status === 400) {
return res.text().then(text => Promise.reject(text))
}
return res.text() return res.text()
}) })
} }

View File

@ -468,6 +468,8 @@ export const getProviders = () =>
export const codeOwnersNotFoundMessage = 'CODEOWNERS file not found' export const codeOwnersNotFoundMessage = 'CODEOWNERS file not found'
export const codeOwnersNotFoundMessage2 = `path "CODEOWNERS" not found` export const codeOwnersNotFoundMessage2 = `path "CODEOWNERS" not found`
export const codeOwnersNotFoundMessage3 = `failed to find node 'CODEOWNERS' in 'main': failed to get tree node: failed to ls file: path "CODEOWNERS" not found` export const codeOwnersNotFoundMessage3 = `failed to find node 'CODEOWNERS' in 'main': failed to get tree node: failed to ls file: path "CODEOWNERS" not found`
export const oldCommitRefetchRequired = 'A newer commit is available. Only the latest commit can be merged.'
export const prMergedRefetchRequired = 'Pull request already merged'
export const dryMerge = ( export const dryMerge = (
isMounted: React.MutableRefObject<boolean>, isMounted: React.MutableRefObject<boolean>,
@ -504,6 +506,7 @@ export const dryMerge = (
pullRequestSection: string | undefined, pullRequestSection: string | undefined,
showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void, showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void,
setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>, setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>,
refetchPullReq: () => void,
setRequiresCommentApproval?: (value: React.SetStateAction<boolean>) => void, setRequiresCommentApproval?: (value: React.SetStateAction<boolean>) => void,
setAtLeastOneReviewerRule?: (value: React.SetStateAction<boolean>) => void, setAtLeastOneReviewerRule?: (value: React.SetStateAction<boolean>) => void,
setReqCodeOwnerApproval?: (value: React.SetStateAction<boolean>) => void, setReqCodeOwnerApproval?: (value: React.SetStateAction<boolean>) => void,
@ -555,6 +558,11 @@ export const dryMerge = (
setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest) setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest) setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest)
setConflictingFiles?.(err.conflict_files) setConflictingFiles?.(err.conflict_files)
} else if (
err.status === 400 &&
(getErrorMessage(err) === oldCommitRefetchRequired || getErrorMessage(err) === prMergedRefetchRequired)
) {
refetchPullReq()
} else if ( } else if (
getErrorMessage(err) === codeOwnersNotFoundMessage || getErrorMessage(err) === codeOwnersNotFoundMessage ||
getErrorMessage(err) === codeOwnersNotFoundMessage2 || getErrorMessage(err) === codeOwnersNotFoundMessage2 ||

View File

@ -171,6 +171,7 @@ export interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMeta
PRStateLoading: boolean PRStateLoading: boolean
conflictingFiles: string[] | undefined conflictingFiles: string[] | undefined
setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>> setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>
refetchPullReq: () => void
} }
export interface PRMergeOption extends SelectOption { export interface PRMergeOption extends SelectOption {