mirror of https://github.com/harness/drone.git
479 lines
18 KiB
TypeScript
479 lines
18 KiB
TypeScript
/*
|
|
* 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, { useState } from 'react'
|
|
import { PopoverInteractionKind, Spinner } from '@blueprintjs/core'
|
|
import { useGet, useMutate } from 'restful-react'
|
|
import { omit } from 'lodash-es'
|
|
import cx from 'classnames'
|
|
import { Container, Layout, Text, Avatar, FlexExpander, useToaster, Utils, stringSubstitute } from '@harnessio/uicore'
|
|
import { Icon, IconName } from '@harnessio/icons'
|
|
import { Color, FontVariation } from '@harnessio/design-system'
|
|
import { Render } from 'react-jsx-match'
|
|
import { useAppContext } from 'AppContext'
|
|
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
|
import { useStrings } from 'framework/strings'
|
|
import type { TypesPullReq, RepoRepositoryOutput, EnumPullReqReviewDecision, TypesScopesLabels } from 'services/code'
|
|
import { ColorName, getErrorMessage } from 'utils/Utils'
|
|
import { ReviewerSelect } from 'components/ReviewerSelect/ReviewerSelect'
|
|
import { PullReqReviewDecision, processReviewDecision } from 'pages/PullRequest/PullRequestUtils'
|
|
import { LabelSelector } from 'components/Label/LabelSelector/LabelSelector'
|
|
import { Label } from 'components/Label/Label'
|
|
import { getConfig } from 'services/config'
|
|
import ignoreFailed from '../../../../icons/ignoreFailed.svg?url'
|
|
import css from './PullRequestSideBar.module.scss'
|
|
|
|
interface PullRequestSideBarProps {
|
|
reviewers?: Unknown
|
|
labels: TypesScopesLabels | null
|
|
repoMetadata: RepoRepositoryOutput
|
|
pullRequestMetadata: TypesPullReq
|
|
refetchReviewers: () => void
|
|
refetchLabels: () => void
|
|
refetchActivities: () => void
|
|
}
|
|
|
|
const PullRequestSideBar = (props: PullRequestSideBarProps) => {
|
|
const { standalone, hooks } = useAppContext()
|
|
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
|
|
const [labelQuery, setLabelQuery] = useState<string>('')
|
|
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers, labels, refetchLabels, refetchActivities } =
|
|
props
|
|
const { getString } = useStrings()
|
|
const { showError, showSuccess } = useToaster()
|
|
const generateReviewDecisionInfo = (
|
|
reviewDecision: EnumPullReqReviewDecision | PullReqReviewDecision.outdated
|
|
): {
|
|
name: IconName
|
|
color?: Color
|
|
size?: number
|
|
icon: IconName
|
|
className?: string
|
|
iconProps?: { color?: Color }
|
|
message: string
|
|
} => {
|
|
let info: {
|
|
name: IconName
|
|
color?: Color
|
|
size?: number
|
|
className?: string
|
|
icon: IconName
|
|
iconProps?: { color?: Color }
|
|
message: string
|
|
}
|
|
|
|
switch (reviewDecision) {
|
|
case PullReqReviewDecision.changeReq:
|
|
info = {
|
|
name: 'error-transparent-no-outline',
|
|
color: Color.RED_700,
|
|
size: 18,
|
|
className: css.redIcon,
|
|
icon: 'error-transparent-no-outline',
|
|
iconProps: { color: Color.RED_700 },
|
|
message: 'requested changes'
|
|
}
|
|
break
|
|
case PullReqReviewDecision.approved:
|
|
info = {
|
|
name: 'tick-circle',
|
|
color: Color.GREEN_700,
|
|
size: 16,
|
|
icon: 'tick-circle',
|
|
iconProps: { color: Color.GREEN_700 },
|
|
message: 'approved changes'
|
|
}
|
|
break
|
|
case PullReqReviewDecision.pending:
|
|
info = {
|
|
name: 'waiting',
|
|
color: Color.GREY_700,
|
|
size: 16,
|
|
icon: 'waiting',
|
|
iconProps: { color: Color.GREY_700 },
|
|
message: 'pending review'
|
|
}
|
|
break
|
|
case PullReqReviewDecision.outdated:
|
|
info = {
|
|
name: 'dot',
|
|
color: Color.GREY_100,
|
|
size: 16,
|
|
icon: 'dot',
|
|
message: 'outdated approval'
|
|
}
|
|
break
|
|
default:
|
|
info = {
|
|
name: 'dot',
|
|
color: Color.GREY_100,
|
|
size: 16,
|
|
icon: 'dot',
|
|
message: 'status'
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// const { data: reviewersData,refetch:refetchReviewers } = useGet<Unknown[]>({
|
|
// path: `/api/v1/principals`,
|
|
// queryParams: {
|
|
// query: searchTerm,
|
|
// limit: LIST_FETCHING_LIMIT,
|
|
// page: page,
|
|
// accountIdentifier: 'kmpySmUISimoRrJL6NL73w',
|
|
// type: 'user'
|
|
// }
|
|
// })
|
|
// console.log(reviewers)
|
|
|
|
// interface PrReviewOption {
|
|
// method: 'optional' | 'required' | 'reject'
|
|
// title: string
|
|
// disabled?: boolean
|
|
// color: Color
|
|
// }
|
|
// const prDecisionOptions: PrReviewOption[] = [
|
|
// {
|
|
// method: 'optional',
|
|
// title: getString('optional'),
|
|
// color: Color.GREEN_700
|
|
// },
|
|
// {
|
|
// method: 'required',
|
|
// title: getString('required'),
|
|
// color: Color.ORANGE_700
|
|
// }
|
|
// ]
|
|
const { mutate: updateCodeCommentStatus } = useMutate({
|
|
verb: 'PUT',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/reviewers`
|
|
})
|
|
const { mutate: removeReviewer } = useMutate({
|
|
verb: 'DELETE',
|
|
path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}`
|
|
})
|
|
|
|
const { mutate: removeLabel, loading: removingLabel } = useMutate({
|
|
verb: 'DELETE',
|
|
base: getConfig('code/api/v1'),
|
|
path: ({ label_id }) => `/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/labels/${label_id}`
|
|
})
|
|
|
|
const {
|
|
data: labelsList,
|
|
refetch: refetchlabelsList,
|
|
loading: labelListLoading
|
|
} = useGet<TypesScopesLabels>({
|
|
base: getConfig('code/api/v1'),
|
|
path: `/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/labels`,
|
|
queryParams: { assignable: true, query: labelQuery },
|
|
debounce: 500
|
|
})
|
|
|
|
// const [isOptionsOpen, setOptionsOpen] = React.useState(false)
|
|
// const [val, setVal] = useState<SelectOption>()
|
|
//TODO: add actions when you click the options menu button and also api integration when there's optional and required reviwers
|
|
return (
|
|
<Container width={`30%`}>
|
|
<Container padding={{ left: 'xxlarge' }}>
|
|
<Layout.Vertical>
|
|
<Layout.Horizontal>
|
|
<Text style={{ lineHeight: '24px' }} font={{ variation: FontVariation.H6 }}>
|
|
{getString('reviewers')}
|
|
</Text>
|
|
<FlexExpander />
|
|
|
|
{/* <Popover
|
|
isOpen={isOptionsOpen}
|
|
onInteraction={nextOpenState => {
|
|
setOptionsOpen(nextOpenState)
|
|
}}
|
|
content={
|
|
<Menu>
|
|
{prDecisionOptions.map(option => {
|
|
return (
|
|
<Menu.Item
|
|
key={option.method}
|
|
// className={css.menuReviewItem}
|
|
disabled={false || option.disabled}
|
|
text={
|
|
<Layout.Horizontal>
|
|
<Text flex width={'fit-content'} font={{ variation: FontVariation.BODY }}>
|
|
{option.title}
|
|
</Text>
|
|
</Layout.Horizontal>
|
|
}
|
|
onClick={() => {
|
|
// setDecisionOption(option)
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</Menu>
|
|
}
|
|
usePortal={true}
|
|
minimal={true}
|
|
fill={false}
|
|
position={Position.BOTTOM_RIGHT}> */}
|
|
<ReviewerSelect
|
|
pullRequestMetadata={pullRequestMetadata}
|
|
onSelect={function (id: number): void {
|
|
updateCodeCommentStatus({ reviewer_id: id }).catch(err => {
|
|
showError(getErrorMessage(err))
|
|
})
|
|
if (refetchReviewers) {
|
|
refetchReviewers()
|
|
}
|
|
}}></ReviewerSelect>
|
|
{/* </Popover> */}
|
|
</Layout.Horizontal>
|
|
<Container padding={{ top: 'medium', bottom: 'large' }}>
|
|
{/* <Text
|
|
className={css.semiBoldText}
|
|
padding={{ bottom: 'medium' }}
|
|
font={{ variation: FontVariation.FORM_LABEL, size: 'small' }}>
|
|
{getString('required')}
|
|
</Text> */}
|
|
{reviewers && reviewers?.length !== 0 ? (
|
|
reviewers.map(
|
|
(reviewer: {
|
|
reviewer: { display_name: string; id: number }
|
|
review_decision: EnumPullReqReviewDecision
|
|
sha: string
|
|
}): Unknown => {
|
|
const updatedReviewDecision = processReviewDecision(
|
|
reviewer.review_decision,
|
|
reviewer.sha,
|
|
pullRequestMetadata?.source_sha
|
|
)
|
|
const reviewerInfo = generateReviewDecisionInfo(updatedReviewDecision)
|
|
return (
|
|
<Layout.Horizontal key={reviewer.reviewer.id} className={css.alignLayout}>
|
|
<Utils.WrapOptionalTooltip
|
|
tooltip={
|
|
<Text color={Color.GREY_100} padding="small">
|
|
{reviewerInfo.message}
|
|
</Text>
|
|
}
|
|
tooltipProps={{ isDark: true, interactionKind: PopoverInteractionKind.HOVER }}>
|
|
{updatedReviewDecision === PullReqReviewDecision.outdated ? (
|
|
<img className={css.svgOutdated} src={ignoreFailed} width={20} height={20} />
|
|
) : (
|
|
<Icon {...omit(reviewerInfo, 'iconProps')} />
|
|
)}
|
|
</Utils.WrapOptionalTooltip>
|
|
<Avatar
|
|
className={cx(css.reviewerAvatar, {
|
|
[css.iconPadding]: updatedReviewDecision !== PullReqReviewDecision.changeReq
|
|
})}
|
|
name={reviewer.reviewer.display_name}
|
|
size="small"
|
|
hoverCard={false}
|
|
/>
|
|
|
|
<Text lineClamp={1} className={css.reviewerName}>
|
|
{reviewer.reviewer.display_name}
|
|
</Text>
|
|
<FlexExpander />
|
|
<OptionsMenuButton
|
|
isDark={true}
|
|
icon="Options"
|
|
iconProps={{ size: 14 }}
|
|
style={{ paddingBottom: '9px' }}
|
|
// disabled={!!commentItem?.deleted}
|
|
width="100px"
|
|
height="24px"
|
|
items={[
|
|
// {
|
|
// text: getString('makeOptional'),
|
|
// onClick: noop
|
|
// },
|
|
// {
|
|
// text: getString('makeRequired'),
|
|
// onClick: noop
|
|
// },
|
|
// '-',
|
|
{
|
|
isDanger: true,
|
|
text: getString('remove'),
|
|
onClick: () => {
|
|
removeReviewer({}, { pathParams: { id: reviewer.reviewer.id } }).catch(err => {
|
|
showError(getErrorMessage(err))
|
|
})
|
|
if (refetchReviewers) {
|
|
refetchReviewers?.()
|
|
}
|
|
}
|
|
}
|
|
]}
|
|
/>
|
|
</Layout.Horizontal>
|
|
)
|
|
}
|
|
)
|
|
) : (
|
|
<Text color={Color.GREY_300} font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
|
{getString('noReviewers')}
|
|
</Text>
|
|
)}
|
|
{/* <Text
|
|
className={css.semiBoldText}
|
|
padding={{ top: 'medium', bottom: 'medium' }}
|
|
font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
|
{getString('optional')}
|
|
</Text>
|
|
{reviewers && reviewers?.length !== 0 ? (
|
|
reviewers.map((reviewer: { reviewer: { display_name: string; id: number }; review_decision: string }) => {
|
|
return (
|
|
<Layout.Horizontal key={reviewer.reviewer.id}>
|
|
<Icon className={css.reviewerPadding} name="dot" />
|
|
<Avatar
|
|
className={css.reviewerAvatar}
|
|
name={reviewer.reviewer.display_name}
|
|
size="small"
|
|
hoverCard={false}
|
|
/>
|
|
|
|
<Text className={css.reviewerName}>{reviewer.reviewer.display_name}</Text>
|
|
<FlexExpander />
|
|
<OptionsMenuButton
|
|
isDark={true}
|
|
icon="Options"
|
|
iconProps={{ size: 14 }}
|
|
style={{ paddingBottom: '9px' }}
|
|
// disabled={!!commentItem?.deleted}
|
|
width="100px"
|
|
height="24px"
|
|
items={[
|
|
{
|
|
text: getString('makeOptional'),
|
|
onClick: noop
|
|
},
|
|
{
|
|
text: getString('makeRequired'),
|
|
onClick: noop
|
|
},
|
|
'-',
|
|
{
|
|
isDanger: true,
|
|
text: getString('remove'),
|
|
onClick: noop
|
|
}
|
|
]}
|
|
/>
|
|
</Layout.Horizontal>
|
|
)
|
|
})
|
|
) : (
|
|
<Text color={Color.GREY_300} font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
|
{getString('noOptionalReviewers')}
|
|
</Text>
|
|
)} */}
|
|
</Container>
|
|
{/* <Layout.Horizontal>
|
|
<Text style={{ lineHeight: '24px' }} font={{ variation: FontVariation.H6 }}>
|
|
{getString('tags')}
|
|
</Text>
|
|
<FlexExpander />
|
|
<Button text={'Add +'} size={ButtonSize.SMALL} variation={ButtonVariation.TERTIARY}></Button>
|
|
</Layout.Horizontal>
|
|
{tagArr.length !== 0 ? (
|
|
<></>
|
|
) : (
|
|
<Text
|
|
font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}
|
|
padding={{ top: 'large', bottom: 'large' }}>
|
|
{getString('noneYet')}
|
|
</Text>
|
|
)} */}
|
|
</Layout.Vertical>
|
|
<Render when={isLabelEnabled || standalone}>
|
|
<Layout.Vertical>
|
|
<Layout.Horizontal>
|
|
<Text style={{ lineHeight: '24px' }} font={{ variation: FontVariation.H6 }}>
|
|
{getString('labels.labels')}
|
|
</Text>
|
|
<FlexExpander />
|
|
|
|
<LabelSelector
|
|
pullRequestMetadata={pullRequestMetadata}
|
|
allLabelsData={labelsList}
|
|
refetchLabels={refetchLabels}
|
|
refetchlabelsList={refetchlabelsList}
|
|
repoMetadata={repoMetadata}
|
|
query={labelQuery}
|
|
setQuery={setLabelQuery}
|
|
labelListLoading={labelListLoading}
|
|
refetchActivities={refetchActivities}
|
|
/>
|
|
</Layout.Horizontal>
|
|
<Container padding={{ top: 'medium', bottom: 'large' }}>
|
|
<Layout.Horizontal className={css.labelsLayout}>
|
|
{labels && labels.label_data?.length !== 0 ? (
|
|
labels?.label_data?.map((label, index) => (
|
|
<Label
|
|
key={index}
|
|
name={label.key as string}
|
|
label_color={label.color as ColorName}
|
|
label_value={{
|
|
name: label.assigned_value?.value as string,
|
|
color: label.assigned_value?.color as ColorName
|
|
}}
|
|
scope={label.scope}
|
|
removeLabelBtn={true}
|
|
disableRemoveBtnTooltip={true}
|
|
handleRemoveClick={() => {
|
|
removeLabel({}, { pathParams: { label_id: label.id } })
|
|
.then(() => {
|
|
label.assigned_value?.value
|
|
? showSuccess(
|
|
stringSubstitute(getString('labels.removedLabel'), {
|
|
label: `${label.key}:${label.assigned_value?.value}`
|
|
}) as string
|
|
)
|
|
: showSuccess(
|
|
stringSubstitute(getString('labels.removedLabel'), {
|
|
label: label.key
|
|
}) as string
|
|
)
|
|
refetchActivities()
|
|
})
|
|
.catch(err => {
|
|
showError(getErrorMessage(err))
|
|
})
|
|
refetchLabels()
|
|
}}
|
|
/>
|
|
))
|
|
) : (
|
|
<Text color={Color.GREY_300} font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
|
{getString('labels.noLabels')}
|
|
</Text>
|
|
)}
|
|
{removingLabel && <Spinner size={16} />}
|
|
</Layout.Horizontal>
|
|
</Container>
|
|
</Layout.Vertical>
|
|
</Render>
|
|
</Container>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
export default PullRequestSideBar
|