labels support in Gitness (#2456)

* fix: lint check
* fix: page overflow
* resolve comments
* resolve comments
* fix: space label addition
* resolve comments : added enum
* update: settings -> manage repository
* update: resolved comments
* update: bugbash comments
* fix: ref exact scope for individual label values call
* fix lint
* add hook to handle current scope in HC and fix scope filter
* update space delete
* prettier check
* update labelAPIs to use getConfig in base
* support for harness-code labels
* fix no result card
* resolved comments for types
* resolved comments
* added sorting in labels and handled edge cases
* fix: replacement of any value label
* fix: spacing in value filter search input
* add: update modal on click for spaces
* added search for values in filter
* fix: UI issues and some enhancements
* handle empty labels list in space and remove tooltip for PR labels
* added getConifg and any values for label filter
* change label value color to enum
* make value-id a pionter
* update ordering
* expose value id
* handle long values
* update search in label selector
* handle edge cases
* fix lint
* added FF : CODE_PULLREQ_LABELS and standalone flag to labels
* fix popover on scrolling and added strings
* fix checks
* fix checks
* swagger update
* labels support in Gitness
This commit is contained in:
Ritik Kapoor 2024-08-20 21:05:11 +00:00 committed by Harness
parent d2dcc9213b
commit 2d14111677
56 changed files with 9153 additions and 835 deletions

View File

@ -24,6 +24,7 @@ import (
"github.com/harness/gitness/types/enum"
"github.com/Masterminds/squirrel"
"github.com/gotidy/ptr"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
@ -56,9 +57,11 @@ type pullReqAssignmentInfo struct {
LabelID int64 `db:"label_id"`
LabelKey string `db:"label_key"`
LabelColor enum.LabelColor `db:"label_color"`
LabelScope int64 `db:"label_scope"`
ValueCount int64 `db:"label_value_count"`
ValueID null.Int `db:"label_value_id"`
Value null.String `db:"label_value_value"`
ValueColor null.String `db:"label_value_color"`
ValueColor null.String `db:"label_value_color"` // get's converted to *enum.LabelColor
}
const (
@ -193,7 +196,9 @@ func (s *pullReqLabelStore) ListAssignedByPullreqIDs(
,label_id
,label_key
,label_color
,label_scope
,label_value_count
,label_value_id
,label_value_value
,label_value_color
`).
@ -261,14 +266,20 @@ func mapPullReqLabel(lbl *pullReqLabel) *types.PullReqLabel {
}
func mapPullReqAssignmentInfo(lbl *pullReqAssignmentInfo) *types.LabelPullReqAssignmentInfo {
var valueColor *enum.LabelColor
if lbl.ValueColor.Valid {
valueColor = ptr.Of(enum.LabelColor(lbl.ValueColor.String))
}
return &types.LabelPullReqAssignmentInfo{
PullReqID: lbl.PullReqID,
LabelID: lbl.LabelID,
LabelKey: lbl.LabelKey,
LabelColor: lbl.LabelColor,
LabelScope: lbl.LabelScope,
ValueCount: lbl.ValueCount,
ValueID: lbl.ValueID.Ptr(),
Value: lbl.Value.Ptr(),
ValueColor: lbl.ValueColor.Ptr(),
ValueColor: valueColor,
}
}

View File

@ -95,13 +95,15 @@ type LabelAssignment struct {
}
type LabelPullReqAssignmentInfo struct {
PullReqID int64 `json:"-"`
LabelID int64 `json:"id"`
LabelKey string `json:"key"`
LabelColor enum.LabelColor `json:"color,omitempty"`
ValueCount int64 `json:"value_count"`
Value *string `json:"value,omitempty"`
ValueColor *string `json:"value_color,omitempty"`
PullReqID int64 `json:"-"`
LabelID int64 `json:"id"`
LabelKey string `json:"key"`
LabelColor enum.LabelColor `json:"color,omitempty"`
LabelScope int64 `json:"scope"`
ValueCount int64 `json:"value_count"`
ValueID *int64 `json:"value_id,omitempty"`
Value *string `json:"value,omitempty"`
ValueColor *enum.LabelColor `json:"value_color,omitempty"`
}
type ScopeData struct {

View File

@ -52,6 +52,7 @@ module.exports = {
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
'./Search': './src/pages/Search/CodeSearchPage.tsx',
'./Labels': './src/pages/ManageSpace/ManageLabels/ManageLabels.tsx',
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
},

View File

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

View File

@ -54,6 +54,7 @@ import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails'
import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing'
import GitspaceCreate from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate'
import ManageLabels from 'pages/ManageSpace/ManageLabels/ManageLabels'
export const RouteDestinations: React.FC = React.memo(function RouteDestinations() {
const { getString } = useStrings()
@ -81,7 +82,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
</LayoutWithSideNav>
</Route>
<Route path={routes.toCODESpaceSettings({ space: pathProps.space })} exact>
<Route
path={[
routes.toCODESpaceSettings({ space: pathProps.space, settingSection: pathProps.settingSection }),
routes.toCODESpaceSettings({ space: pathProps.space })
]}
exact>
<LayoutWithSideNav title={getString('pageTitle.spaceSettings')}>
<SpaceSettings />
</LayoutWithSideNav>
@ -375,6 +381,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
</LayoutWithSideNav>
</Route>
<Route path={routes.toCODESpaceLabels({ space: pathProps.space })} exact>
<LayoutWithSideNav title={getString('labels.labels')}>
<ManageLabels />
</LayoutWithSideNav>
</Route>
<Route
path={[routes.toCODESpaceSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
exact>

View File

@ -34,7 +34,14 @@ export enum CommentType {
MERGE = 'merge',
BRANCH_UPDATE = 'branch-update',
BRANCH_DELETE = 'branch-delete',
STATE_CHANGE = 'state-change'
STATE_CHANGE = 'state-change',
LABEL_MODIFY = 'label-modify'
}
export enum LabelActivity {
ASSIGN = 'assign',
UN_ASSIGN = 'unassign',
RE_ASSIGN = 'reassign'
}
/**

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.
*/
.labelTag {
border: none !important;
padding: 0px !important;
.labelKey {
padding-left: 7px !important;
padding-right: 7px !important;
border-radius: 4px 0 0 4px !important;
background-color: inherit !important;
display: flex !important;
align-items: center;
gap: 4px;
}
.labelValue {
padding: 1px 7px !important;
border: 1px transparent !important;
border-radius: 0 4px 4px 0 !important;
gap: 4px;
white-space: nowrap !important;
.labelValueTxt {
max-width: 150px !important;
}
}
.standaloneKey {
padding-left: 7px !important;
padding-right: 7px !important;
border-radius: 4px !important;
background-color: var(--grey-0) !important;
gap: 4px;
}
}
.removeBtnTag {
:global {
.bp3-button {
cursor: pointer !important;
--button-height: 15px !important;
min-width: 16px !important ;
}
}
}
.labelKey :global {
.bp3-tag {
background-color: inherit !important;
}
}
.popover {
border-radius: 8px !important;
}
.danger {
&:not(.isDark) {
* {
color: var(--red-500) !important;
}
}
&:hover {
background-color: var(--red-500) !important;
* {
color: var(--white) !important;
}
}
}
.isDark {
.icon {
svg path {
fill: var(--white) !important;
}
}
}
.valuesList {
flex-wrap: wrap;
gap: 7px;
width: 100%;
}

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const danger: string
export declare const icon: string
export declare const isDark: string
export declare const labelKey: string
export declare const labelTag: string
export declare const labelValue: string
export declare const labelValueTxt: string
export declare const popover: string
export declare const removeBtnTag: string
export declare const standaloneKey: string
export declare const valuesList: string

View File

@ -0,0 +1,336 @@
/*
* 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, { useEffect } from 'react'
import cx from 'classnames'
import { Button, ButtonSize, ButtonVariation, Container, Layout, Tag, Text } from '@harnessio/uicore'
import { FontVariation } from '@harnessio/design-system'
import { useGet } from 'restful-react'
import { Menu } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons'
import { isEmpty } from 'lodash-es'
import { ColorName, LabelType, getColorsObj, getScopeData, getScopeIcon } from 'utils/Utils'
import type { RepoRepositoryOutput, TypesLabelValue } from 'services/code'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import { getConfig } from 'services/config'
import css from './Label.module.scss'
interface Label {
name: string
scope?: number
label_color?: ColorName
}
interface LabelTitleProps extends Label {
labelType?: LabelType
value_count?: number
}
interface LabelProps extends Label {
label_value?: {
name?: string
color?: ColorName
}
className?: string
removeLabelBtn?: boolean
handleRemoveClick?: () => void
onClick?: () => void
disableRemoveBtnTooltip?: boolean
}
export const Label: React.FC<LabelProps> = props => {
const {
name,
scope,
label_value: { name: valueName, color: valueColor } = {},
label_color,
className,
removeLabelBtn,
handleRemoveClick,
onClick,
disableRemoveBtnTooltip = false
} = props
const { getString } = useStrings()
const { standalone } = useAppContext()
const scopeIcon = getScopeIcon(scope, standalone)
if (valueName) {
const colorObj = getColorsObj(valueColor ?? label_color ?? ColorName.Blue)
return (
<Tag
onClick={e => {
if (onClick) {
onClick()
e.stopPropagation()
}
}}
className={cx(css.labelTag, className, { [css.removeBtnTag]: removeLabelBtn })}>
<Layout.Horizontal flex={{ alignItems: 'center' }}>
<Container
style={{
border: `1px solid ${colorObj.stroke}`
}}
className={css.labelKey}>
{scopeIcon && (
<Icon
name={scopeIcon}
size={12}
style={{
color: `${colorObj.text}`
}}
/>
)}
<Text
style={{
color: `${colorObj.text}`
}}
font={{ variation: FontVariation.SMALL_SEMI }}
lineClamp={1}>
{name}
</Text>
</Container>
<Layout.Horizontal
className={css.labelValue}
style={{
backgroundColor: `${colorObj.backgroundWithoutStroke}`
}}
flex={{ alignItems: 'center' }}>
<Text
style={{
color: `${colorObj.text}`,
backgroundColor: `${colorObj.backgroundWithoutStroke}`
}}
lineClamp={1}
className={css.labelValueTxt}
font={{ variation: FontVariation.SMALL_SEMI }}>
{valueName}
</Text>
{removeLabelBtn && (
<Button
variation={ButtonVariation.ICON}
minimal
icon="main-close"
role="close"
color={colorObj.backgroundWithoutStroke}
iconProps={{ size: 8 }}
size={ButtonSize.SMALL}
onClick={() => {
if (handleRemoveClick && disableRemoveBtnTooltip) handleRemoveClick()
}}
tooltip={
<Menu style={{ minWidth: 'unset' }}>
<Menu.Item
text={getString('labels.removeLabel')}
key={getString('labels.removeLabel')}
className={cx(css.danger, css.isDark)}
onClick={handleRemoveClick}
/>
</Menu>
}
tooltipProps={{ disabled: disableRemoveBtnTooltip, interactionKind: 'click', isDark: true }}
/>
)}
</Layout.Horizontal>
</Layout.Horizontal>
</Tag>
)
} else {
const colorObj = getColorsObj(label_color ?? ColorName.Blue)
return (
<Tag
onClick={e => {
if (onClick) {
onClick()
e.stopPropagation()
}
}}
className={cx(css.labelTag, className, { [css.removeBtnTag]: removeLabelBtn })}>
<Layout.Horizontal
className={css.standaloneKey}
flex={{ alignItems: 'center' }}
style={{
color: `${colorObj.text}`,
border: `1px solid ${colorObj.stroke}`
}}>
{scopeIcon && (
<Icon
name={scopeIcon}
size={12}
style={{
color: `${colorObj.text}`
}}
/>
)}
<Text
style={{
color: `${colorObj.text}`
}}
lineClamp={1}
font={{ variation: FontVariation.SMALL_SEMI }}>
{name}
</Text>
{removeLabelBtn && (
<Button
variation={ButtonVariation.ICON}
minimal
icon="main-close"
role="close"
color={colorObj.backgroundWithoutStroke}
iconProps={{ size: 8 }}
size={ButtonSize.SMALL}
onClick={() => {
if (handleRemoveClick && disableRemoveBtnTooltip) handleRemoveClick()
}}
tooltip={
<Menu style={{ minWidth: 'unset' }}>
<Menu.Item
text={getString('labels.removeLabel')}
key={getString('labels.removeLabel')}
className={cx(css.danger, css.isDark)}
onClick={handleRemoveClick}
/>
</Menu>
}
tooltipProps={{ disabled: disableRemoveBtnTooltip, interactionKind: 'click', isDark: true }}
/>
)}
</Layout.Horizontal>
</Tag>
)
}
}
export const LabelTitle: React.FC<LabelTitleProps> = props => {
const { name, scope, label_color, value_count, labelType } = props
const { standalone } = useAppContext()
const colorObj = getColorsObj(label_color ?? ColorName.Blue)
const scopeIcon = getScopeIcon(scope, standalone)
if (value_count || (labelType && labelType === LabelType.DYNAMIC)) {
return (
<Tag className={css.labelTag}>
<Layout.Horizontal flex={{ alignItems: 'center' }}>
<Container
style={{
border: `1px solid ${colorObj.stroke}`
}}
className={css.labelKey}>
{scopeIcon && (
<Icon
style={{
color: `${colorObj.text}`
}}
name={scopeIcon}
size={12}
/>
)}
<Text
style={{
color: `${colorObj.text}`
}}
lineClamp={1}
font={{ variation: FontVariation.SMALL_SEMI }}>
{name}
</Text>
</Container>
<Text
style={{
color: `${colorObj.text}`,
backgroundColor: `${colorObj.backgroundWithoutStroke}`
}}
className={css.labelValue}
font={{ variation: FontVariation.SMALL_SEMI }}>
... ({value_count ?? 0})
</Text>
</Layout.Horizontal>
</Tag>
)
} else {
return (
<Tag className={css.labelTag}>
<Layout.Horizontal
className={css.standaloneKey}
flex={{ alignItems: 'center' }}
style={{
border: `1px solid ${colorObj.stroke}`
}}>
{scopeIcon && (
<Icon
style={{
color: `${colorObj.text}`
}}
name={scopeIcon}
size={12}
/>
)}
<Text
style={{
color: `${colorObj.text}`
}}
lineClamp={1}
font={{ variation: FontVariation.SMALL_SEMI }}>
{name}
</Text>
</Layout.Horizontal>
</Tag>
)
}
}
export const LabelValuesList: 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/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(name)}/values`
: `/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(name)}/values`
const {
data: labelValues,
refetch: refetchLabelValues,
loading: loadingLabelValues
} = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'),
path: getPath(),
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 }}
/>
))
) : (
<Icon name="steps-spinner" size={16} />
)}
</Layout.Horizontal>
)
}

View File

@ -0,0 +1,154 @@
.labelDropdownPopover {
max-width: 250px !important ;
:global {
.bp3-popover-wrapper {
width: 100% !important;
.bp3-popover-target {
width: 100% !important;
}
}
}
}
.labelCtn {
width: 100% !important;
padding-left: 2px !important;
padding-right: 5px !important;
:global {
.bp3-button {
justify-content: space-between !important;
}
.bp3-icon-chevron-down {
margin-left: auto !important;
}
}
}
.labelCtn:hover {
background-color: var(--primary-1);
cursor: pointer;
}
.labelBtn,
.maxWidth {
width: 100% !important;
}
.labelBtn {
padding-left: 10px !important;
.inputBox {
border: none !important;
outline: none !important;
}
}
.offsetcheck {
color: var(--primary-7) !important;
}
.parentBox {
position: relative;
:global {
.bp3-button {
transition: none !important;
}
}
}
.childBox {
width: fit-content !important;
max-width: 250px;
position: absolute;
box-shadow: 0px 0px 1px rgb(40 41 61 / 4%), 0px 2px 4px rgb(96 97 112 / 16%);
border: 0.5px solid rgba(0, 0, 0, 0.1);
border-radius: var(--spacing-2);
top: -17px !important;
left: 3px !important;
padding: 0px !important;
max-height: 400px;
overflow: scroll;
padding: var(--spacing-xsmall) !important;
}
.childBox > :first-child {
margin-bottom: 0 !important;
}
.hide {
display: none;
}
.menuItem {
width: 207px;
padding-left: 9px;
padding-right: 9px;
margin-top: 1px;
margin-bottom: 1px;
&.selected {
background-color: var(--grey-100) !important;
}
}
.highlight {
background-color: var(--primary-1) !important;
}
.menuItem:hover {
background-color: var(--primary-1) !important;
color: var(--grey-800);
}
.popover {
box-shadow: var(--elevation-4) !important;
> div[class*='popover-arrow'] {
display: none;
}
}
.counter {
font-size: var(--font-size-small) !important;
font-weight: 500 !important;
background-color: var(--primary-7) !important;
color: var(--white) !important;
padding: 0 var(--spacing-small) !important;
border-radius: 10px !important;
display: inline-flex !important;
height: 17px !important;
align-items: center !important;
}
.closeBtn {
cursor: pointer;
position: absolute;
right: 5px;
top: 20px;
}
.input {
padding: var(--spacing-xsmall) !important;
:global {
.bp3-icon {
width: 35px;
padding-left: 2px;
height: 100%;
display: flex !important;
justify-content: center;
align-items: center;
margin: 0 !important;
}
.bp3-input {
background-color: var(--grey-100);
border: 1px solid var(--grey-100);
box-shadow: none !important;
}
.bp3-input:focus,
.bp3-input:active,
.bp3-input:hover {
border: 1px solid var(--grey-100) !important;
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const childBox: string
export declare const closeBtn: string
export declare const counter: string
export declare const hide: string
export declare const highlight: string
export declare const input: string
export declare const inputBox: string
export declare const labelBtn: string
export declare const labelCtn: string
export declare const labelDropdownPopover: string
export declare const maxWidth: string
export declare const menuItem: string
export declare const offsetcheck: string
export declare const parentBox: string
export declare const popover: string
export declare const selected: string

View File

@ -0,0 +1,571 @@
/*
* 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, { useEffect, useState } from 'react'
import {
Container,
Layout,
FlexExpander,
DropDown,
ButtonVariation,
Button,
Text,
SelectOption,
useToaster,
stringSubstitute,
TextInput
} from '@harnessio/uicore'
import cx from 'classnames'
import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { Menu, MenuItem, PopoverInteractionKind, PopoverPosition, Spinner } from '@blueprintjs/core'
import { isEmpty, noop } from 'lodash-es'
import { getConfig, getUsingFetch } from 'services/config'
import type { EnumLabelColor, RepoRepositoryOutput, TypesLabel, TypesLabelValue } from 'services/code'
import {
ColorName,
LIST_FETCHING_LIMIT,
LabelFilterObj,
LabelFilterType,
getErrorMessage,
getScopeData
} from 'utils/Utils'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
import { Label, LabelTitle } from '../Label'
import css from './LabelFilter.module.scss'
interface LabelFilterProps {
labelFilterOption?: LabelFilterObj[]
setLabelFilterOption: React.Dispatch<React.SetStateAction<LabelFilterObj[] | undefined>>
onPullRequestLabelFilterChanged: (labelFilter: LabelFilterObj[]) => void
bearerToken: string
repoMetadata: RepoRepositoryOutput
spaceRef: string
}
enum utilFilterType {
LABEL = 'label',
VALUE = 'value',
FOR_VALUE = 'for_value'
}
const mapToSelectOptions = (items: TypesLabelValue[] | TypesLabel[] = []) =>
items.map(item => ({
label: JSON.stringify(item),
value: String(item?.id)
})) as SelectOption[]
export const LabelFilter = (props: LabelFilterProps) => {
const {
labelFilterOption,
setLabelFilterOption,
onPullRequestLabelFilterChanged,
bearerToken,
repoMetadata,
spaceRef
} = props
const { showError } = useToaster()
const { standalone } = useAppContext()
const [loadingLabels, setLoadingLabels] = useState(false)
const [loadingLabelValues, setLoadingLabelValues] = useState(false)
const [labelValues, setLabelValues] = useState<SelectOption[]>()
const [labelQuery, setLabelQuery] = useState<string>('')
const [highlightItem, setHighlightItem] = useState('')
const [isVisible, setIsVisible] = useState(true)
const [valueQuery, setValueQuery] = useState('')
const [labelItems, setLabelItems] = useState<SelectOption[]>()
const { getString } = useStrings()
const getDropdownLabels = async (currentFilterOption?: LabelFilterObj[]) => {
try {
const fetchedLabels: TypesLabel[] = await getUsingFetch(
getConfig('code/api/v1'),
`/repos/${repoMetadata?.path}/+/labels`,
bearerToken,
{
queryParams: {
page: 1,
limit: LIST_FETCHING_LIMIT,
inherited: true,
query: labelQuery?.trim()
}
}
)
const updateLabelsList = mapToSelectOptions(fetchedLabels)
const labelForTop = mapToSelectOptions(currentFilterOption?.map(({ labelObj }) => labelObj))
const mergedArray = [...labelForTop, ...updateLabelsList]
return Array.from(new Map(mergedArray.map(item => [item.value, item])).values())
} catch (error) {
showError(getErrorMessage(error))
}
}
useEffect(() => {
setLoadingLabels(true)
getDropdownLabels(labelFilterOption)
.then(res => {
setLabelItems(res)
})
.finally(() => {
setLoadingLabels(false)
})
.catch(error => showError(getErrorMessage(error)))
}, [labelFilterOption, labelQuery])
const getLabelValuesPromise = async (key: string, scope: number): Promise<SelectOption[]> => {
setLoadingLabelValues(true)
const { scopeRef } = getScopeData(spaceRef, scope, standalone)
if (scope === 0) {
try {
const fetchedValues: TypesLabelValue[] = await getUsingFetch(
getConfig('code/api/v1'),
`/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(key)}/values`,
bearerToken,
{}
)
const updatedValuesList = mapToSelectOptions(fetchedValues)
setLoadingLabelValues(false)
return updatedValuesList
} catch (error) {
setLoadingLabelValues(false)
showError(getErrorMessage(error))
return []
}
} else {
try {
const fetchedValues: TypesLabelValue[] = await getUsingFetch(
getConfig('code/api/v1'),
`/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(key)}/values`,
bearerToken,
{}
)
const updatedValuesList = Array.isArray(fetchedValues)
? ([
...(fetchedValues || []).map(item => ({
label: JSON.stringify(item),
value: String(item?.id)
}))
] as SelectOption[])
: ([] as SelectOption[])
setLoadingLabelValues(false)
return updatedValuesList
} catch (error) {
setLoadingLabelValues(false)
showError(getErrorMessage(error))
return []
}
}
}
const containsFilter = (filterObjArr: LabelFilterObj[], currentObj: any, type: utilFilterType) => {
let res = false
if (filterObjArr && filterObjArr.length === 0) return res
else if (type === utilFilterType.LABEL) {
res = filterObjArr.some(
filterObj =>
filterObj.labelId === currentObj.id &&
filterObj.valueId === undefined &&
filterObj.type === LabelFilterType.LABEL
)
} else if (type === utilFilterType.VALUE) {
const labelId = currentObj?.valueId === -1 ? currentObj.labelId : currentObj.label_id
const valueId = currentObj?.valueId === -1 ? currentObj.valueId : currentObj.id
res = filterObjArr.some(
filterObj =>
filterObj.labelId === labelId && filterObj.valueId === valueId && filterObj.type === LabelFilterType.VALUE
)
} else if (type === utilFilterType.FOR_VALUE) {
res = filterObjArr.some(
filterObj =>
filterObj.labelId === currentObj.id &&
filterObj.valueId !== undefined &&
filterObj.type === LabelFilterType.VALUE
)
}
return res
}
const replaceValueFilter = (filterObjArr: LabelFilterObj[], currentObj: any) => {
const updateFilterObjArr = filterObjArr.map(filterObj => {
if (filterObj.labelId === currentObj.label_id && filterObj.type === LabelFilterType.VALUE) {
return { ...filterObj, valueId: currentObj.id, valueObj: currentObj }
}
return filterObj
})
onPullRequestLabelFilterChanged([...updateFilterObjArr])
setLabelFilterOption([...updateFilterObjArr])
}
const removeValueFromFilter = (filterObjArr: LabelFilterObj[], currentObj: any) => {
const updateFilterObjArr = filterObjArr.filter(filterObj => {
if (!(filterObj.labelId === currentObj.label_id && filterObj.type === LabelFilterType.VALUE)) {
return filterObj
}
})
onPullRequestLabelFilterChanged(updateFilterObjArr)
setLabelFilterOption(updateFilterObjArr)
}
const removeLabelFromFilter = (filterObjArr: LabelFilterObj[], currentObj: any) => {
const updateFilterObjArr = filterObjArr.filter(filterObj => {
if (!(filterObj.labelId === currentObj.id && filterObj.type === LabelFilterType.LABEL)) {
return filterObj
}
})
onPullRequestLabelFilterChanged(updateFilterObjArr)
setLabelFilterOption(updateFilterObjArr)
}
return (
<DropDown
value={
labelFilterOption && !isEmpty(labelFilterOption)
? (labelFilterOption[labelFilterOption.length - 1].labelId as unknown as string)
: (labelFilterOption?.length as unknown as string)
}
items={labelItems}
disabled={loadingLabels}
onChange={noop}
popoverClassName={css.labelDropdownPopover}
icon={!isEmpty(labelFilterOption) ? undefined : 'code-tag'}
iconProps={{ size: 16 }}
placeholder={getString('labels.filterByLabels')}
resetOnClose
resetOnSelect
resetOnQuery
query={labelQuery}
onQueryChange={newQuery => {
setLabelQuery(newQuery)
}}
itemRenderer={(item, { handleClick }) => {
const itemObj = JSON.parse(item.label)
const offsetValue = labelFilterOption && containsFilter(labelFilterOption, itemObj, utilFilterType.FOR_VALUE)
const offsetLabel = labelFilterOption && containsFilter(labelFilterOption, itemObj, utilFilterType.LABEL)
const anyValueObj = {
labelId: itemObj.id as number,
type: LabelFilterType.VALUE,
valueId: -1,
labelObj: itemObj,
valueObj: {
id: -1,
color: itemObj.color as EnumLabelColor,
label_id: itemObj.id,
value: getString('labels.anyValueOption')
}
}
const filteredLabelValues = (filterQuery: string) => {
if (!filterQuery) {
return labelValues
}
const lowerCaseQuery = filterQuery.toLowerCase()
return labelValues?.filter((value: any) => {
const valueObj = JSON.parse(value.label)
return valueObj.value?.toLowerCase().includes(lowerCaseQuery)
})
}
const labelsValueList = filteredLabelValues(valueQuery)
return (
<Container
onMouseEnter={() => (item.label !== highlightItem ? setIsVisible(false) : setIsVisible(true))}
className={cx(css.labelCtn, { [css.highlight]: highlightItem === item.label })}>
{itemObj.value_count ? (
<Button
className={css.labelBtn}
text={
labelFilterOption?.length ? (
<Layout.Horizontal
className={css.offsetcheck}
spacing={'small'}
flex={{ alignItems: 'center', justifyContent: 'space-between' }}
width={'100%'}>
<Icon name={'tick'} size={16} style={{ opacity: offsetValue ? 1 : 0 }} />
<FlexExpander />
<LabelTitle
name={itemObj?.key as string}
value_count={itemObj.value_count}
label_color={itemObj.color as ColorName}
scope={itemObj.scope}
/>
</Layout.Horizontal>
) : (
<LabelTitle
name={itemObj?.key as string}
value_count={itemObj.value_count}
label_color={itemObj.color as ColorName}
scope={itemObj.scope}
/>
)
}
rightIcon={'chevron-right'}
iconProps={{ size: 16 }}
variation={ButtonVariation.LINK}
onClick={e => {
e.preventDefault()
setIsVisible(true)
setValueQuery('')
setHighlightItem(item.label as string)
getLabelValuesPromise(itemObj.key, itemObj.scope)
.then(res => setLabelValues(res))
.catch(err => {
showError(getErrorMessage(err))
})
}}
tooltip={
labelsValueList && !loadingLabelValues ? (
<Menu key={itemObj.id} className={css.childBox}>
<TextInput
className={css.input}
wrapperClassName={css.inputBox}
value={valueQuery}
autoFocus
placeholder={getString('labels.findaValue')}
onInput={e => {
const _value = e.currentTarget.value || ''
setValueQuery(_value)
}}
leftIcon={'thinner-search'}
leftIconProps={{
name: 'thinner-search',
size: 12,
color: Color.GREY_500
}}
rightElement={valueQuery ? 'main-close' : undefined}
rightElementProps={{
onClick: () => setValueQuery(''),
className: css.closeBtn,
size: 8,
color: Color.GREY_300
}}
/>
<MenuItem
key={itemObj.key + getString('labels.anyValue')}
onClick={event => {
if (offsetValue) {
if (containsFilter(labelFilterOption, anyValueObj, utilFilterType.VALUE)) {
removeValueFromFilter(labelFilterOption, anyValueObj.valueObj)
} else {
replaceValueFilter(labelFilterOption, anyValueObj.valueObj)
}
} else if (labelFilterOption) {
onPullRequestLabelFilterChanged([...labelFilterOption, anyValueObj])
setLabelFilterOption([...labelFilterOption, anyValueObj])
}
handleClick(event)
}}
className={cx(css.menuItem)}
text={
offsetValue ? (
<Layout.Horizontal
className={css.offsetcheck}
spacing={'small'}
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
width={'100%'}>
<Icon
name={'tick'}
size={16}
color={Color.PRIMARY_7}
style={{
opacity: containsFilter(labelFilterOption, anyValueObj, utilFilterType.VALUE) ? 1 : 0
}}
/>
<Label
key={itemObj.key + (anyValueObj.valueObj.value as string)}
name={itemObj.key}
label_value={{
name: anyValueObj.valueObj.value,
color: anyValueObj.valueObj.color as ColorName
}}
scope={itemObj.scope}
/>
</Layout.Horizontal>
) : (
<Label
key={itemObj.key + (anyValueObj.valueObj.value as string)}
name={itemObj.key}
label_value={{
name: anyValueObj.valueObj.value,
color: anyValueObj.valueObj.color as ColorName
}}
scope={itemObj.scope}
/>
)
}
/>
{labelsValueList.map(value => {
const valueObj = JSON.parse(value.label)
const currentMarkedValue = labelFilterOption
? containsFilter(labelFilterOption, valueObj, utilFilterType.VALUE)
: {}
const updatedValueFilterOption = labelFilterOption
? [
...labelFilterOption,
{
labelId: valueObj.label_id,
valueId: valueObj.id,
type: LabelFilterType.VALUE,
labelObj: itemObj,
valueObj: valueObj
}
]
: []
return (
<MenuItem
key={itemObj.key + (value.value as string) + 'menu'}
onClick={event => {
if (offsetValue) {
if (currentMarkedValue) {
removeValueFromFilter(labelFilterOption, valueObj)
} else {
replaceValueFilter(labelFilterOption, valueObj)
}
} else {
onPullRequestLabelFilterChanged(updatedValueFilterOption)
setLabelFilterOption(updatedValueFilterOption)
}
handleClick(event)
}}
className={cx(css.menuItem)}
text={
offsetValue ? (
<Layout.Horizontal
className={css.offsetcheck}
spacing={'small'}
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
width={'100%'}>
<Icon
name={'tick'}
size={16}
color={Color.PRIMARY_7}
style={{ opacity: currentMarkedValue ? 1 : 0 }}
/>
<Label
key={itemObj.key + (value.value as string)}
name={itemObj.key}
label_value={{ name: valueObj.value, color: valueObj.color as ColorName }}
scope={itemObj.scope}
/>
</Layout.Horizontal>
) : (
<Label
key={itemObj.key + (value.value as string)}
name={itemObj.key}
label_value={{ name: valueObj.value, color: valueObj.color as ColorName }}
scope={itemObj.scope}
/>
)
}
/>
)
})}
</Menu>
) : (
<Menu className={css.menuItem} style={{ justifyContent: 'center' }}>
<Spinner size={20} />
</Menu>
)
}
tooltipProps={{
interactionKind: PopoverInteractionKind.CLICK,
position: PopoverPosition.RIGHT,
popoverClassName: cx(css.popover, { [css.hide]: !isVisible }),
modifiers: { preventOverflow: { boundariesElement: 'viewport' } }
}}
/>
) : (
<Container
onClick={event => {
handleClick(event)
const updatedLabelFilterOption = Array.isArray(labelFilterOption)
? [
...labelFilterOption,
{
labelId: itemObj.id,
valueId: undefined,
type: LabelFilterType.LABEL,
labelObj: itemObj,
valueObj: undefined
}
]
: ([] as LabelFilterObj[] | undefined)
if (offsetLabel) removeLabelFromFilter(labelFilterOption, itemObj)
else {
onPullRequestLabelFilterChanged(
updatedLabelFilterOption ? [...updatedLabelFilterOption] : ([] as LabelFilterObj[])
)
setLabelFilterOption(
updatedLabelFilterOption ? [...updatedLabelFilterOption] : ([] as LabelFilterObj[])
)
}
}}>
<Button
className={css.labelBtn}
text={
labelFilterOption?.length ? (
<Layout.Horizontal
className={css.offsetcheck}
spacing={'small'}
flex={{ alignItems: 'center', justifyContent: 'space-between' }}
width={'100%'}>
<Icon name={'tick'} size={16} style={{ opacity: offsetLabel ? 1 : 0 }} />
<FlexExpander />
<LabelTitle
name={itemObj?.key as string}
value_count={itemObj.value_count}
label_color={itemObj.color as ColorName}
scope={itemObj.scope}
/>
</Layout.Horizontal>
) : (
<Layout.Horizontal>
<LabelTitle
name={itemObj?.key as string}
value_count={itemObj.value_count}
label_color={itemObj.color as ColorName}
scope={itemObj.scope}
/>
</Layout.Horizontal>
)
}
variation={ButtonVariation.LINK}
/>
</Container>
)}
</Container>
)
}}
getCustomLabel={() => {
return (
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }}>
<Text className={css.counter}>{labelFilterOption?.length}</Text>
<Text lineClamp={1} color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
{
stringSubstitute(getString('labels.labelsApplied'), {
labelCount: labelFilterOption?.length
}) as string
}
</Text>
</Layout.Horizontal>
)
}}
/>
)
}

View File

@ -0,0 +1,136 @@
.addLabelBtn {
--background-color-active: var(--white) !important;
--box-shadow: none !important;
&:active,
&:hover,
&[class*='bp3-active'] {
--box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.04), 0px 2px 4px rgba(96, 97, 112, 0.16) !important;
}
.prefix {
color: var(--grey-450) !important;
font-weight: normal;
}
}
.main {
min-width: 241px;
max-width: 376px;
.layout {
> [class*='TextInput--main'] {
margin-bottom: 0 !important;
}
.menuContainer {
margin-top: 0 !important;
}
}
}
.popover {
box-shadow: var(--elevation-4) !important;
> div[class*='popover-arrow'] {
display: none;
}
}
.noWrapText {
white-space: nowrap !important;
.valueNotFound {
color: var(--primary-7) !important;
cursor: pointer;
}
}
.labelMenu {
min-height: fit-content !important;
max-height: 386px !important;
overflow: scroll;
padding-left: 0px !important ;
padding-right: 0px !important ;
:global {
.bp3-menu-item:hover {
background: var(--grey-50) !important;
color: var(--grey-1000) !important;
}
.bp3-menu-item.bp3-active {
background: var(--grey-100) !important;
color: var(--grey-1000) !important;
}
}
.menuItem {
padding-left: 9px !important;
padding-right: 0 !important;
margin-top: 1px;
margin-bottom: 1px;
&.selected {
background-color: var(--grey-100) !important;
}
}
}
.labelSearch {
display: block;
box-sizing: border-box;
border: 1px solid var(--grey-200);
border-radius: var(--spacing-2);
font-size: var(--form-input-font-size);
padding-left: var(--spacing-small) !important;
color: var(--black);
box-shadow: none;
:global {
[class*='TextInput--main'] {
margin-bottom: 0 !important;
height: inherit !important;
width: fit-content !important;
flex-grow: 1 !important;
border: none !important;
outline: none !important;
}
.bp3-input {
border: none !important;
outline: none !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.bp3-input:hover {
box-shadow: none !important;
}
}
.labelCtn {
max-width: 50% !important;
}
.labelKey {
border: none !important;
padding: 0 !important;
.labelInput {
padding: 0px 0px 0px 11px !important;
border-radius: 4px 0 0 4px !important;
background-color: var(--grey-0) !important;
height: 32px !important;
}
}
.input {
margin-bottom: 0px !important;
}
.inputBox {
border: none !important;
outline: none !important;
}
}
.labelSearch:focus {
color: var(--black);
border-color: var(--primary-7);
box-shadow: none;
}
.closeBtn {
cursor: pointer;
margin: 5px 2.5px !important;
}

View File

@ -0,0 +1,36 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const addLabelBtn: string
export declare const closeBtn: string
export declare const input: string
export declare const inputBox: string
export declare const labelCtn: string
export declare const labelInput: string
export declare const labelKey: string
export declare const labelMenu: string
export declare const labelSearch: string
export declare const layout: string
export declare const main: string
export declare const menuContainer: string
export declare const menuItem: string
export declare const noWrapText: string
export declare const popover: string
export declare const prefix: string
export declare const selected: string
export declare const valueNotFound: string

View File

@ -0,0 +1,555 @@
/*
* 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, { useEffect, useRef, useState } from 'react'
import {
Button,
ButtonProps,
ButtonSize,
ButtonVariation,
Container,
Layout,
Tag,
Text,
TextInput,
useToaster
} from '@harnessio/uicore'
import cx from 'classnames'
import { Menu, MenuItem, PopoverPosition } from '@blueprintjs/core'
import { useMutate } from 'restful-react'
import { Render } from 'react-jsx-match'
import { isEmpty } from 'lodash-es'
import { useAppContext } from 'AppContext'
import type {
RepoRepositoryOutput,
TypesLabelAssignment,
TypesLabelValueInfo,
TypesPullReq,
TypesScopesLabels
} from 'services/code'
import { useStrings } from 'framework/strings'
import { ButtonRoleProps, ColorName, LabelType, getErrorMessage, permissionProps } from 'utils/Utils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { getConfig } from 'services/config'
import { Label, LabelTitle } from '../Label'
import css from './LabelSelector.module.scss'
export interface LabelSelectorProps {
allLabelsData: TypesScopesLabels | null
refetchLabels: () => void
refetchlabelsList: () => void
repoMetadata: RepoRepositoryOutput
pullRequestMetadata: TypesPullReq
setQuery: React.Dispatch<React.SetStateAction<string>>
query: string
labelListLoading: boolean
}
export interface LabelSelectProps extends Omit<ButtonProps, 'onSelect'> {
onSelectLabel: (label: TypesLabelAssignment) => void
onSelectValue: (labelId: number, valueId: number, labelKey: string, valueKey: string) => void
menuState?: LabelsMenuState
currentLabel: TypesLabelAssignment
handleValueRemove?: () => void
addNewValue?: () => void
allLabelsData: TypesScopesLabels | null
query: string
setQuery: React.Dispatch<React.SetStateAction<string>>
menuItemIndex: number
setMenuItemIndex: React.Dispatch<React.SetStateAction<number>>
labelListLoading: boolean
}
enum LabelsMenuState {
LABELS = 'labels',
VALUES = 'label_values'
}
export const LabelSelector: React.FC<LabelSelectorProps> = ({
allLabelsData,
refetchLabels,
pullRequestMetadata,
repoMetadata,
refetchlabelsList,
query,
setQuery,
labelListLoading,
...props
}) => {
const [popoverDialogOpen, setPopoverDialogOpen] = useState<boolean>(false)
const [menuState, setMenuState] = useState<LabelsMenuState>(LabelsMenuState.LABELS)
const [menuItemIndex, setMenuItemIndex] = useState<number>(0)
const [currentLabel, setCurrentLabel] = useState<TypesLabelAssignment>({ key: '', id: -1 })
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const { mutate: updatePRLabels } = useMutate({
verb: 'PUT',
base: getConfig('code/api/v1'),
path: `/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/labels`
})
const space = useGetSpaceParam()
const { hooks, standalone } = useAppContext()
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_edit']
},
[space]
)
return (
<Button
className={css.addLabelBtn}
text={<span className={css.prefix}>{getString('add')}</span>}
variation={ButtonVariation.TERTIARY}
minimal
size={ButtonSize.SMALL}
tooltip={
<PopoverContent
onSelectLabel={label => {
setCurrentLabel(label)
if (label.values?.length || label.type === LabelType.DYNAMIC) {
setMenuState(LabelsMenuState.VALUES)
setMenuItemIndex(0)
} else {
try {
updatePRLabels({
label_id: label.id
})
.then(() => {
refetchLabels()
setQuery('')
setPopoverDialogOpen(false)
showSuccess(`Applied '${label.key}' label`)
})
.catch(error => showError(getErrorMessage(error)))
} catch (exception) {
showError(getErrorMessage(exception))
}
}
}}
onSelectValue={(labelId, valueId, labelKey, valueKey) => {
setMenuState(LabelsMenuState.VALUES)
setMenuItemIndex(0)
try {
updatePRLabels({
label_id: labelId,
value_id: valueId
})
.then(() => {
refetchLabels()
setMenuState(LabelsMenuState.LABELS)
setMenuItemIndex(0)
setCurrentLabel({ key: '', id: -1 })
setPopoverDialogOpen(false)
showSuccess(`Applied '${labelKey}:${valueKey}' label`)
})
.catch(error => showError(getErrorMessage(error)))
} catch (exception) {
showError(getErrorMessage(exception))
}
}}
allLabelsData={allLabelsData}
menuState={menuState}
currentLabel={currentLabel}
addNewValue={() => {
try {
updatePRLabels({
label_id: currentLabel.id,
value: query
})
.then(() => {
showSuccess(`Updated ${currentLabel.key} with ${query}`)
refetchLabels()
refetchlabelsList()
setMenuState(LabelsMenuState.LABELS)
setMenuItemIndex(0)
setCurrentLabel({ key: '', id: -1 })
setPopoverDialogOpen(false)
setQuery('')
})
.catch(error => showError(getErrorMessage(error)))
} catch (exception) {
showError(getErrorMessage(exception))
}
}}
query={query}
setQuery={setQuery}
handleValueRemove={() => {
setMenuState(LabelsMenuState.LABELS)
setMenuItemIndex(0)
setCurrentLabel({ key: '', id: -1 })
}}
menuItemIndex={menuItemIndex}
setMenuItemIndex={setMenuItemIndex}
labelListLoading={labelListLoading}
/>
}
tooltipProps={{
interactionKind: 'click',
usePortal: true,
position: PopoverPosition.BOTTOM_RIGHT,
popoverClassName: css.popover,
isOpen: popoverDialogOpen,
onClose: () => {
setMenuState(LabelsMenuState.LABELS)
setMenuItemIndex(0)
setCurrentLabel({ key: '', id: -1 })
setQuery('')
},
onInteraction: nxtState => setPopoverDialogOpen(nxtState)
}}
tabIndex={0}
{...props}
{...permissionProps(permPushResult, standalone)}
/>
)
}
const PopoverContent: React.FC<LabelSelectProps> = ({
onSelectLabel,
onSelectValue,
menuState,
currentLabel,
handleValueRemove,
allLabelsData,
addNewValue,
query,
setQuery,
menuItemIndex,
setMenuItemIndex,
labelListLoading
}) => {
const inputRef = useRef<HTMLInputElement | null>()
const { getString } = useStrings()
const filteredLabelValues = (valueQuery: string) => {
if (!valueQuery) return currentLabel?.values // If no query, return all names
const lowerCaseQuery = valueQuery.toLowerCase()
return currentLabel?.values?.filter(label => label.value?.toLowerCase().includes(lowerCaseQuery))
}
const labelsValueList = filteredLabelValues(query)
const labelsList = allLabelsData?.label_data ?? []
useEffect(() => {
if (menuState === LabelsMenuState.LABELS && menuItemIndex > 0) {
const previousLabel = labelsList[menuItemIndex - 1]
if (previousLabel && previousLabel.key && previousLabel.id) {
document
.getElementById(previousLabel.key + previousLabel.id)
?.scrollIntoView({ behavior: 'auto', block: 'center' })
}
} else if (menuState === LabelsMenuState.VALUES && menuItemIndex > 0) {
const previousValue = labelsValueList?.[menuItemIndex - 1]
if (previousValue && previousValue.value && previousValue.id) {
const elementId = previousValue.value + previousValue.id
document.getElementById(elementId)?.scrollIntoView({ behavior: 'auto', block: 'center' })
}
}
}, [menuItemIndex, menuState, labelsList, labelsValueList])
const handleKeyDownLabels: React.KeyboardEventHandler<HTMLInputElement> = e => {
if (labelsList && labelsList.length !== 0) {
switch (e.key) {
case 'ArrowDown':
setMenuItemIndex((index: number) => {
return index + 1 > labelsList.length ? 1 : index + 1
})
break
case 'ArrowUp':
setMenuItemIndex((index: number) => {
return index - 1 > 0 ? index - 1 : labelsList.length
})
break
case 'Enter':
if (labelsList[menuItemIndex - 1]) {
onSelectLabel(labelsList[menuItemIndex - 1])
setQuery('')
}
break
default:
break
}
}
}
const handleKeyDownValue: React.KeyboardEventHandler<HTMLInputElement> = e => {
if (e.key === 'Backspace' && !query && currentLabel) {
setQuery('')
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
}
}
}
return (
<Container padding="small" className={css.main}>
<Layout.Vertical className={css.layout}>
{menuState === LabelsMenuState.LABELS ? (
<TextInput
className={css.input}
wrapperClassName={css.inputBox}
value={query}
inputRef={ref => (inputRef.current = ref)}
autoFocus
placeholder={getString('labels.findALabel')}
onInput={e => {
const _value = e.currentTarget.value || ''
setQuery(_value)
}}
rightElement={query ? 'code-close' : undefined}
rightElementProps={{
onClick: () => setQuery(''),
className: css.closeBtn,
size: 20
}}
onKeyDown={handleKeyDownLabels}
/>
) : (
currentLabel &&
handleValueRemove && (
<Layout.Horizontal flex={{ alignItems: 'center' }} className={css.labelSearch}>
<Container className={css.labelCtn}>
<Label
name={currentLabel.key as string}
label_color={currentLabel.color as ColorName}
scope={currentLabel.scope}
/>
</Container>
<TextInput
className={css.input}
onKeyDown={handleKeyDownValue}
wrapperClassName={css.inputBox}
value={query}
inputRef={ref => (inputRef.current = ref)}
defaultValue={query}
autoFocus
placeholder={
currentLabel.type === LabelType.STATIC
? getString('labels.findaValue')
: !isEmpty(currentLabel.values)
? getString('labels.findOrAdd')
: getString('labels.addaValue')
}
onInput={e => {
const _value = e.currentTarget.value || ''
setQuery(_value)
}}
rightElement={query || currentLabel?.key ? 'code-close' : undefined}
rightElementProps={{
onClick: () => {
setQuery('')
handleValueRemove()
},
className: css.closeBtn,
size: 20
}}
/>
</Layout.Horizontal>
)
)}
<Container className={cx(css.menuContainer)}>
<LabelList
onSelectLabel={onSelectLabel}
onSelectValue={onSelectValue}
query={query}
setQuery={setQuery}
menuState={menuState}
currentLabel={currentLabel}
allLabelsData={labelsList}
menuItemIndex={menuItemIndex}
addNewValue={addNewValue}
setMenuItemIndex={setMenuItemIndex}
labelListLoading={labelListLoading}
/>
</Container>
</Layout.Vertical>
</Container>
)
}
interface LabelListProps extends Omit<LabelSelectProps, 'allLabelsData'> {
query: string
setQuery: React.Dispatch<React.SetStateAction<string>>
setLoading?: React.Dispatch<React.SetStateAction<boolean>>
menuItemIndex: number
allLabelsData: TypesLabelAssignment[]
}
const LabelList = ({
onSelectLabel,
onSelectValue,
query,
setQuery,
menuState,
currentLabel,
allLabelsData: labelsList,
menuItemIndex,
addNewValue,
labelListLoading
}: LabelListProps) => {
const { getString } = useStrings()
if (menuState === LabelsMenuState.LABELS) {
if (labelsList.length) {
return (
<Menu className={css.labelMenu}>
{labelsList?.map((label, index: number) => {
return (
<MenuItem
key={(label.key as string) + label.id}
id={(label.key as string) + label.id}
className={cx(css.menuItem, {
[css.selected]: index === menuItemIndex - 1
})}
text={
<LabelTitle
name={label.key as string}
value_count={label.values?.length}
label_color={label.color as ColorName}
scope={label.scope}
labelType={label.type as LabelType}
/>
}
onClick={e => {
e.preventDefault()
e.stopPropagation()
onSelectLabel(label)
setQuery('')
}}
{...ButtonRoleProps}
/>
)
})}
</Menu>
)
} else {
return (
<Container flex={{ align: 'center-center' }} padding="large">
{!labelListLoading && (
<Text className={css.noWrapText} flex padding={{ top: 'small' }}>
<span>
{query && <Tag> {query} </Tag>} {getString('labels.labelNotFound')}
</span>
</Text>
)}
</Container>
)
}
} else {
const filteredLabelValues = (filterQuery: string) => {
if (!filterQuery) return currentLabel?.values // If no query, return all names
const lowerCaseQuery = filterQuery.toLowerCase()
return currentLabel?.values?.filter(label => label.value?.toLowerCase().includes(lowerCaseQuery))
}
const matchFound = (userQuery: string, list?: TypesLabelValueInfo[]) => {
const res = list ? list.map(ele => ele.value?.toLowerCase()).includes(userQuery.toLowerCase()) : false
return res
}
const labelsValueList = filteredLabelValues(query)
return (
<Menu className={css.labelMenu}>
<Render when={labelsValueList && currentLabel}>
{labelsValueList?.map(({ value, id, color }, index: number) => (
<MenuItem
key={((value as string) + id) as string}
id={((value as string) + id) as string}
className={cx(css.menuItem, {
[css.selected]: index === menuItemIndex - 1
})}
text={
<Label
name={currentLabel.key as string}
label_color={currentLabel.color as ColorName}
label_value={{ name: value as string, color: color as ColorName }}
scope={currentLabel.scope}
/>
}
onClick={e => {
e.preventDefault()
e.stopPropagation()
onSelectValue(currentLabel.id as number, id as number, currentLabel.key as string, value as string)
setQuery('')
}}
{...ButtonRoleProps}
/>
))}
</Render>
<Render when={currentLabel.type === LabelType.DYNAMIC && !matchFound(query, labelsValueList) && query}>
<Button
variation={ButtonVariation.LINK}
className={css.noWrapText}
flex
padding={{ top: 'small', left: 'small' }}
onClick={() => {
if (addNewValue) {
addNewValue()
}
}}>
<span className={css.valueNotFound}>
{getString('labels.addNewValue')}
{currentLabel && (
<Label name={'...'} label_color={currentLabel.color as ColorName} label_value={{ name: query }} />
)}
</span>
</Button>
</Render>
<Render when={labelsValueList?.length === 0 && currentLabel?.type === LabelType.STATIC}>
<Text className={css.noWrapText} flex padding={{ top: 'small', left: 'small' }}>
<span>
{currentLabel && query && (
<Label
name={currentLabel?.key as string}
label_color={currentLabel.color as ColorName}
label_value={{ name: query }}
scope={currentLabel.scope}
/>
)}
{getString('labels.labelNotFound')}
</span>
</Text>
</Render>
</Menu>
)
}
}

View File

@ -34,14 +34,18 @@ interface NoResultCardProps {
onButtonClick?: () => void
permissionProp?: { disabled: boolean; tooltip: JSX.Element | string } | undefined
standalone?: boolean
forFilter?: boolean
emptyFilterMessage?: string
}
export const NoResultCard: React.FC<NoResultCardProps> = ({
showWhen = () => true,
forSearch,
forFilter = false,
title,
message,
emptySearchMessage,
emptyFilterMessage,
buttonText = '',
buttonIcon = CodeIcon.Add,
onButtonClick = noop,
@ -57,12 +61,16 @@ export const NoResultCard: React.FC<NoResultCardProps> = ({
<Container className={css.main}>
<NoDataCard
image={Images.EmptyState}
messageTitle={forSearch ? title || getString('noResultTitle') : undefined}
messageTitle={forSearch || forFilter ? title || getString('noResultTitle') : undefined}
message={
forSearch ? emptySearchMessage || getString('noResultMessage') : message || getString('noResultMessage')
forSearch
? emptySearchMessage || getString('noResultMessage')
: forFilter
? emptyFilterMessage || getString('noFilterResultMessage')
: message || getString('noResultMessage')
}
button={
forSearch ? undefined : (
forSearch || forFilter ? undefined : (
<Button
variation={ButtonVariation.PRIMARY}
text={buttonText}

View File

@ -616,6 +616,60 @@ export interface StringsMap {
'keywordSearch.sampleQueries.searchForPattern': string
keywordSearchPlaceholder: string
killed: string
'labels.addNewValue': string
'labels.addValue': string
'labels.addaValue': string
'labels.allowDynamic': string
'labels.anyValue': string
'labels.anyValueOption': string
'labels.applied': string
'labels.appliedLabel': string
'labels.canbeAddedByUsers': string
'labels.createLabel': string
'labels.createdIn': string
'labels.deleteLabel': string
'labels.deleteLabelConfirm': string
'labels.descriptionOptional': string
'labels.failedToDeleteLabel': string
'labels.filterByLabels': string
'labels.findALabel': string
'labels.findOrAdd': string
'labels.findaValue': string
'labels.intentText': string
'labels.label': string
'labels.labelCreated': string
'labels.labelCreationFailed': string
'labels.labelName': string
'labels.labelNameReq': string
'labels.labelNotFound': string
'labels.labelPreview': string
'labels.labelTo': string
'labels.labelUpdateFailed': string
'labels.labelUpdated': string
'labels.labelValue': string
'labels.labelValueReq': string
'labels.labelValuesOptional': string
'labels.labels': string
'labels.labelsApplied': string
'labels.newLabel': string
'labels.noLabels': string
'labels.noLabelsFound': string
'labels.noNewLine': string
'labels.noRepoLabelsFound': string
'labels.noResults': string
'labels.noScopeLabelsFound': string
'labels.placeholderDescription': string
'labels.prCount': string
'labels.provideLabelName': string
'labels.provideLabelValue': string
'labels.removeLabel': string
'labels.removed': string
'labels.removedLabel': string
'labels.scopeMessage': string
'labels.showLabelsScope': string
'labels.stringMax': string
'labels.updateLabel': string
'labels.updated': string
language: string
leaveAComment: string
license: string
@ -627,6 +681,7 @@ export interface StringsMap {
makeRequired: string
manageApiToken: string
manageCredText: string
manageRepository: string
markAsDraft: string
matchPassword: string
mergeBranchTitle: string
@ -679,6 +734,7 @@ export interface StringsMap {
noCommitsPR: string
noExpiration: string
noExpirationDate: string
noFilterResultMessage: string
noOptionalReviewers: string
noRequiredReviewers: string
noResultMessage: string
@ -899,6 +955,7 @@ export interface StringsMap {
public: string
pullMustBeMadeFromBranches: string
pullRequestEmpty: string
pullRequestNotFoundforFilter: string
pullRequestalreadyExists: string
pullRequests: string
quote: string

View File

@ -0,0 +1,32 @@
/*
* 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 { useParams } from 'react-router-dom'
import { LabelsPageScope } from 'utils/Utils'
import { useAppContext } from 'AppContext'
import type { Identifier } from 'utils/types'
export function useGetCurrentPageScope() {
const { routingId: accountIdentifier, standalone } = useAppContext()
const { orgIdentifier, projectIdentifier } = useParams<Identifier>()
if (standalone) return LabelsPageScope.SPACE
else if (projectIdentifier) return LabelsPageScope.PROJECT
else {
if (orgIdentifier) return LabelsPageScope.ORG
else if (accountIdentifier) LabelsPageScope.ACCOUNT
}
return LabelsPageScope.ACCOUNT
}

View File

@ -30,6 +30,7 @@ commits: Commits
commitChanges: Commit changes
pullRequests: Pull Requests
settings: Settings
manageRepository: Manage Repository
newFile: New File
editFile: Edit File
prev: Prev
@ -90,6 +91,7 @@ cloneHTTPS: Git clone URL
nameYourFile: Name your file...
noResultTitle: Sorry, no result found
noResultMessage: What you searched was unfortunately not found or doesnt exist
noFilterResultMessage: No results were found based on the applied filters
pageTitle:
signin: Sign In
register: Register a new account
@ -216,6 +218,7 @@ newFileNotAllowed: You must be on a branch to create a new file
fileDeleted: '__path__ successfully deleted.'
newPullRequest: New Pull Request
pullRequestEmpty: There are no pull requests in your repo. Click the button below to create a pull request.
pullRequestNotFoundforFilter: No pull requests match the applied filters
comparingChanges: Comparing Changes
selectToViewMore: Select branch to view more here.
createPullRequest: Create pull request
@ -1293,3 +1296,58 @@ regex:
enabled: enabled
disabled: disabled
string: RegEx
labels:
labels: Labels
newLabel: New Label
labelName: Label Name
labelValue: Label Value
labelNotFound: ' label not found'
allowDynamic: Allow users to add values
addNewValue: 'Add new value '
addValue: 'Add value'
removeLabel: Remove Label
deleteLabel: Delete Label
provideLabelName: Provide Label name
labelCreated: Label created
labelUpdated: Label updated
labelNameReq: Label Name is required
labelValueReq: Label Value is required
noLabels: No Labels
updated: 'updated '
applied: 'applied '
label: ' label'
labelTo: ' label to '
removed: 'removed '
anyValue: 'anyvalue'
anyValueOption: '(any value)'
failedToDeleteLabel: Failed to delete Label
findALabel: Find a label
canbeAddedByUsers: '*can be added by users*'
removedLabel: Removed '{label}' label
showLabelsScope: Show labels from parent scopes
createLabel: Create Label
updateLabel: Update Label
appliedLabel: Create a <strong>new branch</strong> for this commit and start a pull request
labelsApplied: '{labelCount|1:Label,Labels} Applied'
createdIn: Created In
labelCreationFailed: Failed to create label
labelUpdateFailed: Failed to update label
noLabelsFound: No Labels found. Click on the button below to create a Label.
noRepoLabelsFound: There are no Labels in your repo. Click the button below to create a Label.
noScopeLabelsFound: There are no Labels present in current scope. Click the button below to create a Label.
scopeMessage: (Showing labels created at current scope or higher)
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.
prCount: 'Showing {count} {count|1:result,results}'
noResults: No results found
labelPreview: Label Preview
filterByLabels: Filter by Label/s
provideLabelValue: 'Provide label value'
findaValue: Find a value
findOrAdd: Find or add a new value
labelValuesOptional: 'Label Value/s (Optional)'
descriptionOptional: 'Description (Optional)'
placeholderDescription: Enter a short description for the label
addaValue: Add a value
stringMax: '{entity} must be 50 characters or less'
noNewLine: '{entity} cannot contain new lines'

View File

@ -155,7 +155,7 @@ export const DefaultMenu: React.FC = () => {
<NavMenuItem
data-code-repo-section="settings"
isSubLink
label={getString('settings')}
label={getString('manageRepository')}
to={routes.toCODESettings({
repoPath
})}

View File

@ -0,0 +1,102 @@
/*
* 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.
*/
.labelModal {
width: 890px;
font-size: 24px;
overflow-y: auto;
padding: 32px 24px;
:global(.bp3-dialog-header) {
margin-bottom: 16px !important;
}
.modalForm {
min-height: 556px !important;
padding: 5px 25px 5px 5px !important;
}
// ToDo: this CSS is for scope box and can be uncommented once scope is implemented in Harness
// .scopeBox {
// margin-top: 4px !important;
// border: 1px solid var(--grey-100);
// border-radius: 3px;
// display: flex;
// align-items: center;
// min-width: 50% !important;
// width: fit-content;
// padding: 2px !important;
// gap: 5px;
// .scopeTag {
// padding: 3px 6px !important;
// font-weight: 400 !important;
// font-size: 12px !important;
// font-family: var(--font-family-inter) !important;
// // background-color: var(--grey-300) !important;
// background-color: var(--grey-100) !important;
// color: var(--primary-7) !important;
// border: none;
// }
// }
.labelDescription textarea {
min-height: 64px !important;
}
}
.popover {
box-shadow: var(--elevation-4);
border-radius: var(--spacing-2);
border: 0.5px solid rgba(0, 0, 0, 0.1);
margin-top: var(--spacing-xsmall);
}
:global {
.bp3-menu {
min-width: 108px !important;
}
}
.colorMenu {
:global {
.bp3-menu-item:hover {
background: var(--grey-50) !important;
color: var(--grey-1000) !important;
}
.bp3-menu-item.bp3-active {
background: var(--grey-100) !important;
color: var(--grey-1000) !important;
}
}
}
.selectColor {
box-shadow: none !important;
background-color: var(--grey-0) !important;
border: 1px solid var(--grey-100) !important;
width: 108px;
padding-right: 0 !important;
padding-left: 15px !important;
}
.yellowContainer {
background: var(--orange-50) !important;
padding: var(--spacing-medium) var(--spacing-small);
margin: 0 var(--spacing-large) 0 0 !important;
border-radius: 4px;
margin-bottom: 5px !important;
}

View File

@ -0,0 +1,25 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const colorMenu: string
export declare const labelDescription: string
export declare const labelModal: string
export declare const modalForm: string
export declare const popover: string
export declare const selectColor: string
export declare const yellowContainer: string

View File

@ -0,0 +1,557 @@
/*
* 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 {
Button,
ButtonVariation,
Dialog,
Layout,
Text,
Container,
useToaster,
Formik,
FormInput,
Popover,
ButtonSize,
FlexExpander,
FormikForm,
stringSubstitute
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { Menu, MenuItem, PopoverInteractionKind, Position } from '@blueprintjs/core'
import * as Yup from 'yup'
import { FieldArray } from 'formik'
import { useGet, useMutate } from 'restful-react'
import { Render } from 'react-jsx-match'
import { useModalHook } from 'hooks/useModalHook'
import { useStrings } from 'framework/strings'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { CodeIcon } from 'utils/GitUtils'
import { colorsPanel, ColorName, ColorDetails, getErrorMessage, LabelType, LabelTypes, getScopeData } from 'utils/Utils'
import { Label } from 'components/Label/Label'
import type {
EnumLabelColor,
TypesLabel,
TypesLabelValue,
TypesSaveLabelInput,
TypesSaveLabelValueInput
} from 'services/code'
import { getConfig } from 'services/config'
import { useAppContext } from 'AppContext'
import css from './LabelModal.module.scss'
const enum ModalMode {
SAVE,
UPDATE
}
interface ExtendedTypesLabelValue extends TypesLabelValue {
color: ColorName
}
interface LabelModalProps {
refetchlabelsList: () => Promise<void>
}
interface LabelFormData extends TypesLabel {
labelName: string
allowDynamicValues: boolean
color: ColorName
labelValues: ExtendedTypesLabelValue[]
}
const ColorSelectorDropdown = (props: {
onClick: (color: ColorName) => void
currentColorName: ColorName | undefined | false
disabled?: boolean
}) => {
const { currentColorName, onClick: onClickColorOption, disabled: disabledPopover } = props
const colorNames: ColorName[] = Object.keys(colorsPanel) as ColorName[]
const getColorsObj = (colorKey: ColorName): ColorDetails => {
return colorsPanel[colorKey]
}
const currentColorObj = getColorsObj(currentColorName ? currentColorName : ColorName.Blue)
return (
<Popover
minimal
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM}
disabled={disabledPopover}
popoverClassName={css.popover}
content={
<Menu style={{ margin: '1px' }} className={css.colorMenu}>
{colorNames?.map(colorName => {
const colorObj = getColorsObj(colorName)
return (
<MenuItem
key={colorName}
active={colorName === currentColorName}
text={
<Text font={{ size: 'normal' }} style={{ color: `${colorObj.text}` }}>
{colorName}
</Text>
}
onClick={() => onClickColorOption(colorName)}
/>
)
})}
</Menu>
}>
<Button
className={css.selectColor}
text={
<Layout.Horizontal width={'97px'} flex={{ alignItems: 'center', justifyContent: 'space-between' }}>
<Text
font={{ size: 'medium' }}
icon={'symbol-circle'}
iconProps={{ size: 20 }}
padding={{ right: 'xsmall' }}
style={{
color: `${currentColorObj.stroke}`
}}
/>
<Text
font={{ size: 'normal' }}
style={{
color: `${currentColorObj.text}`,
gap: '5px',
alignItems: 'center'
}}>
{currentColorName}
</Text>
<FlexExpander />
<Icon
padding={{ right: 'small', top: '2px' }}
name="chevron-down"
font={{ size: 'normal' }}
size={15}
background={currentColorObj.text}
/>
</Layout.Horizontal>
}
/>
</Popover>
)
}
const useLabelModal = ({ refetchlabelsList }: LabelModalProps) => {
const { repoMetadata, space } = useGetRepositoryMetadata()
const { standalone } = useAppContext()
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
const [modalMode, setModalMode] = useState<ModalMode>(ModalMode.SAVE)
const [updateLabel, setUpdateLabel] = useState<LabelTypes>()
const openUpdateLabelModal = (label: LabelTypes) => {
setModalMode(ModalMode.UPDATE)
setUpdateLabel(label)
openModal()
}
const { scopeRef } = getScopeData(space, updateLabel?.scope ?? 1, standalone)
const { mutate: createUpdateLabel } = useMutate({
verb: 'PUT',
base: getConfig('code/api/v1'),
path: `/repos/${repoMetadata?.path as string}/+/labels`
})
const { mutate: createUpdateSpaceLabel } = useMutate({
verb: 'PUT',
base: getConfig('code/api/v1'),
path: updateLabel?.scope ? `/spaces/${scopeRef as string}/+/labels` : `/spaces/${space as string}/+/labels`
})
const {
data: repoLabelValues,
loading: repoValueListLoading,
refetch: refetchRepoValuesList
} = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'),
path: `/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodeURIComponent(
updateLabel?.key ? updateLabel?.key : ''
)}/values`,
lazy: true
})
const {
data: spaceLabelValues,
loading: spaceValueListLoading,
refetch: refetchSpaceValuesList
} = useGet<TypesLabelValue[]>({
base: getConfig('code/api/v1'),
path: `/spaces/${encodeURIComponent(scopeRef)}/labels/${encodeURIComponent(
updateLabel?.key ? updateLabel?.key : ''
)}/values`,
lazy: true
})
const [openModal, hideModal] = useModalHook(() => {
const handleLabelSubmit = (formData: LabelFormData) => {
const { labelName, color, labelValues, description, allowDynamicValues, id } = formData
const createLabelPayload: { label: TypesSaveLabelInput; values: TypesSaveLabelValueInput[] } = {
label: {
color: color?.toLowerCase() as EnumLabelColor,
description: description,
id: id ?? 0,
key: labelName,
type: allowDynamicValues ? LabelType.DYNAMIC : LabelType.STATIC
},
values: labelValues?.length
? labelValues.map(value => {
return {
color: value.color?.toLowerCase() as EnumLabelColor,
id: value.id ?? 0,
value: value.value
}
})
: []
}
if (
(repoMetadata && modalMode === ModalMode.SAVE) ||
(modalMode === ModalMode.UPDATE && updateLabel?.scope === 0 && repoMetadata)
) {
try {
createUpdateLabel(createLabelPayload)
.then(() => {
showSuccess(
modalMode === ModalMode.SAVE ? getString('labels.labelCreated') : getString('labels.labelUpdated')
)
refetchlabelsList()
onClose()
})
.catch(error => showError(getErrorMessage(error), 1200, getString('labels.labelCreationFailed')))
} catch (exception) {
showError(getErrorMessage(exception), 1200, getString('labels.labelCreationFailed'))
}
} else {
try {
createUpdateSpaceLabel(createLabelPayload)
.then(() => {
showSuccess(
modalMode === ModalMode.SAVE ? getString('labels.labelCreated') : getString('labels.labelUpdated')
)
refetchlabelsList()
onClose()
})
.catch(error => showError(getErrorMessage(error), 1200, getString('labels.labelUpdateFailed')))
} catch (exception) {
showError(getErrorMessage(exception), 1200, getString('labels.labelUpdateFailed'))
}
}
}
const onClose = () => {
setModalMode(ModalMode.SAVE)
setUpdateLabel({})
hideModal()
}
const validationSchema = Yup.object({
labelName: Yup.string()
.max(
50,
stringSubstitute(getString('labels.stringMax'), {
entity: 'Name'
}) as string
)
.test(
'no-newlines',
stringSubstitute(getString('labels.noNewLine'), {
entity: 'Name'
}) as string,
value => !/\r|\n/.test(value as string)
)
.required(getString('labels.labelNameReq')),
labelValues: Yup.array().of(
Yup.object({
value: Yup.string()
.max(
50,
stringSubstitute(getString('labels.stringMax'), {
entity: 'Value'
}) as string
)
.test(
'no-newlines',
stringSubstitute(getString('labels.noNewLine'), {
entity: 'Name'
}) as string,
value => !/\r|\n/.test(value as string)
)
.required(getString('labels.labelValueReq')),
color: Yup.string()
})
)
})
const handleKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
}
}
const getLabelValues = (values: TypesLabelValue[] = []) =>
values.map(valueObj => ({
id: valueObj.id,
value: valueObj.value,
color: valueObj.color as ColorName
}))
const initialFormValues = (() => {
const baseValues = {
color: updateLabel?.color as ColorName,
description: updateLabel?.description ?? '',
id: updateLabel?.id ?? 0,
labelName: updateLabel?.key ?? '',
allowDynamicValues: updateLabel?.type === LabelType.DYNAMIC,
labelValues: [] as { id: number; value: string; color: ColorName }[]
}
if (modalMode === ModalMode.SAVE) {
return { ...baseValues, color: ColorName.Blue }
}
if (repoMetadata && updateLabel?.scope === 0) {
return { ...baseValues, labelValues: getLabelValues(repoLabelValues ?? undefined) }
}
return { ...baseValues, labelValues: getLabelValues(spaceLabelValues ?? undefined) }
})()
return (
<Dialog
isOpen
onOpening={() => {
if (modalMode === ModalMode.UPDATE) {
if (repoMetadata && updateLabel?.scope === 0) refetchRepoValuesList()
else refetchSpaceValuesList()
}
}}
enforceFocus={false}
onClose={onClose}
title={modalMode === ModalMode.SAVE ? getString('labels.createLabel') : getString('labels.updateLabel')}
className={css.labelModal}>
<Formik<LabelFormData>
formName="labelModal"
initialValues={initialFormValues}
enableReinitialize={true}
validationSchema={validationSchema}
validateOnChange
validateOnBlur
onSubmit={handleLabelSubmit}>
{formik => {
return (
<FormikForm onKeyDown={handleKeyDown}>
<Render when={modalMode === ModalMode.UPDATE}>
<Container className={css.yellowContainer}>
<Text
icon="main-issue"
iconProps={{ size: 16, color: Color.ORANGE_700, margin: { right: 'small' } }}
padding={{ left: 'large', right: 'large', top: 'small', bottom: 'small' }}
color={Color.WARNING}>
{getString('labels.intentText', {
space: updateLabel?.key
})}
</Text>
</Container>
</Render>
<Layout.Horizontal spacing={'large'}>
<Layout.Vertical style={{ width: '55%' }}>
<Layout.Vertical spacing="large" className={css.modalForm}>
<Container margin={{ top: 'medium' }}>
<Text font={{ variation: FontVariation.BODY2 }}>{getString('labels.labelName')}</Text>
<Layout.Horizontal
flex={{ alignItems: formik.isValid ? 'center' : 'flex-start', justifyContent: 'flex-start' }}
style={{ gap: '4px', margin: '4px' }}>
<ColorSelectorDropdown
currentColorName={formik.values.color || ColorName.Blue}
onClick={(colorName: ColorName) => {
formik.setFieldValue('color', colorName)
}}
/>
<FormInput.Text
key={'labelName'}
style={{ flexGrow: '1', margin: 0 }}
name="labelName"
placeholder={getString('labels.provideLabelName')}
tooltipProps={{
dataTooltipId: 'labels.newLabel'
}}
inputGroup={{ autoFocus: true }}
/>
</Layout.Horizontal>
</Container>
<Container margin={{ top: 'medium' }} className={css.labelDescription}>
<Text font={{ variation: FontVariation.BODY2 }}>{getString('labels.descriptionOptional')}</Text>
<FormInput.Text name="description" placeholder={getString('labels.placeholderDescription')} />
</Container>
<Container margin={{ top: 'medium' }}>
<Text font={{ variation: FontVariation.BODY2 }}>{getString('labels.labelValuesOptional')}</Text>
<FieldArray
name="labelValues"
render={({ push, remove }) => {
return (
<Layout.Vertical>
{formik.values.labelValues?.map((_, index) => (
<Layout.Horizontal
key={`labelValue + ${index}`}
flex={{
alignItems: formik.isValid ? 'center' : 'flex-start',
justifyContent: 'flex-start'
}}
style={{ gap: '4px', margin: '4px' }}>
<ColorSelectorDropdown
key={`labelValueColor + ${index}`}
currentColorName={
formik.values.labelValues &&
index !== undefined &&
(formik.values.labelValues[index].color as ColorName)
}
onClick={(colorName: ColorName) => {
formik.setFieldValue(
'labelValues',
formik.values.labelValues?.map((value, i) =>
i === index ? { ...value, color: colorName } : value
)
)
}}
/>
<FormInput.Text
key={`labelValueKey + ${index}`}
style={{ flexGrow: '1', margin: 0 }}
name={`${'labelValues'}[${index}].value`}
placeholder={getString('labels.provideLabelValue')}
tooltipProps={{
dataTooltipId: 'labels.newLabel'
}}
inputGroup={{ autoFocus: true }}
/>
<Button
key={`removeValue + ${index}`}
style={{ marginRight: 'auto', color: 'var(--grey-300)' }}
variation={ButtonVariation.ICON}
icon={'code-close'}
onClick={() => {
remove(index)
}}
/>
</Layout.Horizontal>
))}
<Button
style={{ marginRight: 'auto' }}
variation={ButtonVariation.LINK}
disabled={!formik.isValid || formik.values.labelName?.length === 0}
text={getString('labels.addValue')}
icon={CodeIcon.Add}
onClick={() =>
push({
name: '',
color: formik.values.color
})
}
/>
</Layout.Vertical>
)
}}
/>
</Container>
<Container margin={{ top: 'medium' }} className={css.labelDescription}>
<FormInput.CheckBox label={getString('labels.allowDynamic')} name="allowDynamicValues" />
</Container>
</Layout.Vertical>
<Container margin={{ top: 'medium' }}>
<Layout.Horizontal flex={{ justifyContent: 'flex-start' }}>
<Button
margin={{ right: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.PRIMARY}
size={ButtonSize.MEDIUM}
/>
<Button
text={getString('cancel')}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.MEDIUM}
onClick={onClose}
/>
</Layout.Horizontal>
</Container>
</Layout.Vertical>
<Layout.Vertical
style={{ width: '45%', padding: '25px 35px 25px 35px', borderLeft: '1px solid var(--grey-100)' }}>
<Text>{getString('labels.labelPreview')}</Text>
<Layout.Vertical spacing={'medium'}>
{formik.values.labelValues?.length
? formik.values.labelValues?.map((valueObj, i) => (
<Label
key={`label + ${i}`}
name={formik.values.labelName || getString('labels.labelName')}
label_color={formik.values.color}
label_value={
valueObj.value?.length
? { name: valueObj.value, color: valueObj.color }
: {
name: getString('labels.labelValue'),
color: valueObj.color || formik.values.color
}
}
/>
))
: !formik.values.allowDynamicValues && (
<Label
name={formik.values.labelName || getString('labels.labelName')}
label_color={formik.values.color}
/>
)}
{formik.values.allowDynamicValues && (
<Label
name={formik.values.labelName || getString('labels.labelName')}
label_color={formik.values.color}
label_value={{ name: getString('labels.canbeAddedByUsers') }}
/>
)}
</Layout.Vertical>
</Layout.Vertical>
</Layout.Horizontal>
</FormikForm>
)
}}
</Formik>
</Dialog>
)
}, [
updateLabel,
repoLabelValues,
spaceLabelValues,
repoValueListLoading,
spaceValueListLoading,
refetchlabelsList,
modalMode
])
return {
openModal,
openUpdateLabelModal,
hideModal
}
}
export default useLabelModal

View File

@ -0,0 +1,64 @@
/*
* 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 {
background-color: var(--primary-bg) !important;
.table {
.row {
height: 80px;
display: flex;
justify-content: center;
.title {
font-weight: 600;
}
}
}
.noData > div {
height: calc(100vh - var(--page-header-height, 64px) - 120px) !important;
}
}
.toggle {
> input {
opacity: 0;
position: relative;
top: 40%;
z-index: 999;
}
}
.toggleEnable {
> input {
left: 52%;
}
}
.toggleDisable {
> input {
left: 32%;
}
}
.cancelButton {
margin-left: var(--spacing-small) !important;
}
.scopeCheckbox {
display: flex;
align-items: center;
padding-right: 7px !important;
padding-left: 7px !important;
}

View File

@ -0,0 +1,28 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const cancelButton: string
export declare const main: string
export declare const noData: string
export declare const row: string
export declare const scopeCheckbox: string
export declare const table: string
export declare const title: string
export declare const toggle: string
export declare const toggleDisable: string
export declare const toggleEnable: string

View File

@ -0,0 +1,107 @@
/*
* 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 { Container, Layout, FlexExpander, ButtonVariation, Button, Checkbox } from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import { useStrings } from 'framework/strings'
import { CodeIcon } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { LabelsPageScope, permissionProps } from 'utils/Utils'
import type { RepoRepositoryOutput } from 'services/code'
import css from './LabelsHeader.module.scss'
const LabelsHeader = ({
loading,
onSearchTermChanged,
showParentScopeFilter,
inheritLabels,
setInheritLabels,
openLabelCreateModal,
spaceRef,
repoMetadata,
currentPageScope
}: LabelsHeaderProps) => {
const [searchTerm, setSearchTerm] = useState('')
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
return (
<Container className={css.main} padding={{ top: 'medium', right: 'xlarge', left: 'xlarge', bottom: 'medium' }}>
<Layout.Horizontal spacing="medium">
<Button
variation={ButtonVariation.PRIMARY}
text={getString('labels.newLabel')}
icon={CodeIcon.Add}
onClick={openLabelCreateModal}
{...permissionProps(permPushResult, standalone)}
/>
<Render when={showParentScopeFilter}>
<Checkbox
className={css.scopeCheckbox}
label={getString('labels.showLabelsScope')}
data-testid={`INCLUDE_ORG_RESOURCES`}
checked={inheritLabels}
onChange={event => {
setInheritLabels(event.currentTarget.checked)
}}
/>
</Render>
<FlexExpander />
<SearchInputWithSpinner
spinnerPosition="right"
loading={loading}
query={searchTerm}
setQuery={value => {
setSearchTerm(value)
onSearchTermChanged(value)
}}
/>
</Layout.Horizontal>
</Container>
)
}
export default LabelsHeader
interface LabelsHeaderProps {
loading?: boolean
activeTab?: string
onSearchTermChanged: (searchTerm: string) => void
repoMetadata?: RepoRepositoryOutput
spaceRef?: string
currentPageScope: LabelsPageScope
showParentScopeFilter: boolean
setInheritLabels: (value: boolean) => void
inheritLabels: boolean
openLabelCreateModal: () => void
}

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 {
.table {
.row {
height: fit-content !important;
display: flex;
justify-content: center;
}
.toggleAccordion {
padding: var(--spacing-large) var(--spacing-medium) !important;
}
.labelCtn {
max-width: 55%;
}
div[class*='TableV2--rowSubComponent'] {
border-top: 1px solid var(--grey-100);
padding: 20px 4px 4px 5%;
}
}
.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;
}
}
}
}
.optionItem {
display: flex;
align-items: center;
svg {
path {
fill: var(--white) !important;
}
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
/* eslint-disable */
// 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 main: string
export declare const optionItem: string
export declare const popover: string
export declare const row: string
export declare const table: string
export declare const toggleAccordion: string
export declare const widthContainer: string

View File

@ -0,0 +1,355 @@
/*
* 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, { useCallback, useEffect, useMemo, useState } from 'react'
import {
Container,
TableV2,
Text,
Button,
ButtonVariation,
useToaster,
StringSubstitute,
Layout,
Utils
} from '@harnessio/uicore'
import type { CellProps, Column, Renderer, Row, UseExpandedRowProps } from 'react-table'
import { useGet, useMutate } from 'restful-react'
import { Color } from '@harnessio/design-system'
import { Intent } from '@blueprintjs/core'
import { useHistory } from 'react-router-dom'
import { Icon } from '@harnessio/icons'
import { isEmpty } from 'lodash-es'
import { useQueryParams } from 'hooks/useQueryParams'
import { usePageIndex } from 'hooks/usePageIndex'
import {
getErrorMessage,
LIST_FETCHING_LIMIT,
permissionProps,
type PageBrowserProps,
ColorName,
LabelTypes,
LabelListingProps,
LabelsPageScope,
getScopeData
} from 'utils/Utils'
import { CodeIcon } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { useStrings, String } from 'framework/strings'
import { useConfirmAction } from 'hooks/useConfirmAction'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { useAppContext } from 'AppContext'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { LabelTitle, LabelValuesList } from 'components/Label/Label'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { getConfig } from 'services/config'
import LabelsHeader from './LabelsHeader/LabelsHeader'
import useLabelModal from './LabelModal/LabelModal'
import css from './LabelsListing.module.scss'
const LabelsListing = (props: LabelListingProps) => {
const { activeTab, currentPageScope, repoMetadata, space } = props
const { hooks, standalone } = useAppContext()
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const history = useHistory()
const pageBrowser = useQueryParams<PageBrowserProps>()
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
const [page, setPage] = usePageIndex(pageInit)
const [searchTerm, setSearchTerm] = useState('')
const [showParentScopeFilter, setShowParentScopeFilter] = useState<boolean>(true)
const [inheritLabels, setInheritLabels] = useState<boolean>(false)
useEffect(() => {
const params = {
...pageBrowser,
...(page > 1 && { page: page.toString() })
}
updateQueryParams(params, undefined, true)
if (page <= 1) {
const updateParams = { ...params }
delete updateParams.page
replaceQueryParams(updateParams, undefined, true)
}
}, [page]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (currentPageScope) {
if (currentPageScope === LabelsPageScope.ACCOUNT) setShowParentScopeFilter(false)
else if (currentPageScope === LabelsPageScope.SPACE) setShowParentScopeFilter(false)
}
}, [currentPageScope, standalone])
const getLabelPath = () =>
currentPageScope === LabelsPageScope.REPOSITORY
? `/repos/${repoMetadata?.path}/+/labels`
: `/spaces/${space}/+/labels`
const {
data: labelsList,
loading: labelsListLoading,
refetch,
response
} = useGet<LabelTypes[]>({
base: getConfig('code/api/v1'),
path: getLabelPath(),
queryParams: {
limit: LIST_FETCHING_LIMIT,
inherited: inheritLabels,
page: page,
query: searchTerm
},
debounce: 500
})
const refetchlabelsList = useCallback(
() =>
refetch({
queryParams: {
limit: LIST_FETCHING_LIMIT,
inherited: inheritLabels,
page: page,
query: searchTerm
}
}),
[inheritLabels, LIST_FETCHING_LIMIT, page, searchTerm]
)
const { openModal: openLabelCreateModal, openUpdateLabelModal } = useLabelModal({ refetchlabelsList })
const renderRowSubComponent = React.useCallback(({ row }: { row: Row<LabelTypes> }) => {
return (
<LabelValuesList
name={row.original?.key as string}
scope={row.original?.scope as number}
repoMetadata={repoMetadata}
space={space}
standalone={standalone}
/>
)
}, [])
const ToggleAccordionCell: Renderer<{
row: UseExpandedRowProps<CellProps<LabelTypes>> & {
original: LabelTypes
}
value: LabelTypes
}> = ({ row }) => {
if (row.original.value_count) {
return (
<Layout.Horizontal onClick={e => e?.stopPropagation()}>
<Button
data-testid="row-expand-btn"
{...row.getToggleRowExpandedProps()}
color={Color.GREY_600}
icon={row.isExpanded ? 'chevron-down' : 'chevron-right'}
variation={ButtonVariation.ICON}
iconProps={{ size: 19 }}
className={css.toggleAccordion}
/>
</Layout.Horizontal>
)
}
return null
}
const columns: Column<LabelTypes>[] = useMemo(
() => [
{
Header: '',
id: 'rowSelectOrExpander',
width: '5%',
Cell: ToggleAccordionCell
},
{
Header: getString('name'),
id: 'name',
sort: 'true',
width: '25%',
Cell: ({ row }: CellProps<LabelTypes>) => {
return (
<Container className={css.labelCtn}>
<LabelTitle
name={row.original?.key as string}
value_count={row.original.value_count}
label_color={row.original.color as ColorName}
scope={row.original.scope}
/>
</Container>
)
}
},
{
Header: getString('labels.createdIn'),
id: 'scope',
sort: 'true',
width: '30%',
Cell: ({ row }: CellProps<LabelTypes>) => {
const { scopeIcon, scopeId } = getScopeData(space as string, row.original.scope ?? 1, standalone)
return (
<Layout.Horizontal spacing={'xsmall'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<Icon size={16} name={row.original.scope === 0 ? CodeIcon.Repo : scopeIcon} />
<Text>{row.original.scope === 0 ? repoMetadata?.identifier : scopeId}</Text>
</Layout.Horizontal>
)
}
},
{
Header: getString('description'),
id: 'description',
width: '40%',
sort: 'true',
Cell: ({ row }: CellProps<LabelTypes>) => {
return <Text lineClamp={3}>{row.original?.description}</Text>
}
},
{
id: 'action',
width: '5%',
Cell: ({ row }: CellProps<LabelTypes>) => {
const encodedLabelKey = row.original.key ? encodeURIComponent(row.original.key) : ''
const { scopeRef } = getScopeData(space as string, row.original?.scope ?? 1, standalone)
const deleteLabelPath =
row.original?.scope === 0
? `/repos/${encodeURIComponent(repoMetadata?.path as string)}/labels/${encodedLabelKey}`
: `/spaces/${encodeURIComponent(scopeRef as string)}/labels/${encodedLabelKey}`
const { mutate: deleteLabel } = useMutate({
verb: 'DELETE',
base: getConfig('code/api/v1'),
path: deleteLabelPath
})
const confirmLabelDelete = useConfirmAction({
title: getString('labels.deleteLabel'),
confirmText: getString('delete'),
intent: Intent.DANGER,
message: <String useRichText stringID="labels.deleteLabelConfirm" vars={{ name: row.original.key }} />,
action: async e => {
e.stopPropagation()
deleteLabel({})
.then(() => {
showSuccess(
<StringSubstitute
str={getString('labels.deleteLabel')}
vars={{
tag: row.original.key
}}
/>,
5000
)
refetchlabelsList()
setPage(1)
})
.catch(error => {
showError(getErrorMessage(error), 0, getString('labels.failedToDeleteLabel'))
})
}
})
return (
<Container margin={{ left: 'medium' }} onClick={Utils.stopEvent}>
<OptionsMenuButton
width="100px"
items={[
{
text: getString('edit'),
iconName: CodeIcon.Edit,
hasIcon: true,
iconSize: 20,
className: css.optionItem,
onClick: () => {
openUpdateLabelModal(row.original)
}
},
{
text: getString('delete'),
iconName: CodeIcon.Delete,
iconSize: 20,
hasIcon: true,
isDanger: true,
className: css.optionItem,
onClick: confirmLabelDelete
}
]}
isDark
/>
</Container>
)
}
}
], // eslint-disable-next-line react-hooks/exhaustive-deps
[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 (
<Container>
<LabelsHeader
activeTab={activeTab}
onSearchTermChanged={(value: React.SetStateAction<string>) => {
setSearchTerm(value)
setPage(1)
}}
showParentScopeFilter={showParentScopeFilter}
inheritLabels={inheritLabels}
setInheritLabels={setInheritLabels}
openLabelCreateModal={openLabelCreateModal}
repoMetadata={repoMetadata}
spaceRef={space}
currentPageScope={currentPageScope}
/>
<Container className={css.main} padding={{ bottom: 'large', right: 'xlarge', left: 'xlarge' }}>
{labelsList && !labelsListLoading && labelsList.length !== 0 && (
<TableV2<LabelTypes>
className={css.table}
columns={columns}
data={labelsList}
sortable
renderRowSubComponent={renderRowSubComponent}
autoResetExpanded={true}
onRowClick={rowData => openUpdateLabelModal(rowData)}
/>
)}
<LoadingSpinner visible={labelsListLoading} />
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
<NoResultCard
showWhen={() => !labelsListLoading && isEmpty(labelsList)}
forSearch={!!searchTerm}
message={getString('labels.noLabelsFound')}
buttonText={getString('labels.newLabel')}
onButtonClick={() => openLabelCreateModal()}
permissionProp={permissionProps(permPushResult, standalone)}
/>
</Container>
)
}
export default LabelsListing

View File

@ -0,0 +1,22 @@
/*
* 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

@ -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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const main: string

View File

@ -0,0 +1,44 @@
/*
* 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

@ -29,6 +29,7 @@ import {
import { useLocation } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react'
import { get, orderBy } from 'lodash-es'
import { Render } from 'react-jsx-match'
import type { GitInfoProps } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import { useAppContext } from 'AppContext'
@ -38,7 +39,8 @@ import type {
TypesPullReqStats,
TypesCodeOwnerEvaluation,
TypesPullReqReviewer,
TypesListCommitResponse
TypesListCommitResponse,
TypesScopesLabels
} from 'services/code'
import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox'
import { useConfirmAct } from 'hooks/useConfirmAction'
@ -50,7 +52,7 @@ import {
filenameToLanguage,
PRCommentFilterType
} from 'utils/Utils'
import { activitiesToDiffCommentItems, activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils'
import { CommentType, activitiesToDiffCommentItems, activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect'
@ -59,6 +61,7 @@ import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondaryS
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
import { getConfig } from 'services/config'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
import { DescriptionBox } from './DescriptionBox'
import PullRequestSideBar from './PullRequestSideBar/PullRequestSideBar'
@ -93,7 +96,8 @@ export const Conversation: React.FC<ConversationProps> = ({
pullReqCommits
}) => {
const { getString } = useStrings()
const { currentUser, routes } = useAppContext()
const { currentUser, routes, hooks } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const location = useLocation()
const activities = usePullReqActivities()
const {
@ -105,6 +109,12 @@ export const Conversation: React.FC<ConversationProps> = ({
debounce: 500
})
const { data: labels, refetch: refetchLabels } = useGet<TypesScopesLabels>({
base: getConfig('code/api/v1'),
path: `/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/labels`,
debounce: 500
})
const { data: codeOwners, refetch: refetchCodeOwners } = useGet<TypesCodeOwnerEvaluation>({
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/codeowners`,
debounce: 500
@ -256,21 +266,24 @@ export const Conversation: React.FC<ConversationProps> = ({
() =>
activityBlocks?.map((commentItems, index) => {
const threadId = commentItems[0].payload?.id
const renderLabelActivities =
commentItems[0].payload?.type !== CommentType.LABEL_MODIFY || isLabelEnabled || standalone
if (isSystemComment(commentItems)) {
return (
<ThreadSection
key={`thread-${threadId}`}
onlyTitle
lastItem={activityBlocks.length - 1 === index}
title={
<SystemComment
key={`system-${threadId}`}
pullReqMetadata={pullReqMetadata}
commentItems={commentItems}
repoMetadataPath={repoMetadata.path}
/>
}></ThreadSection>
<Render key={`thread-${threadId}`} when={renderLabelActivities}>
<ThreadSection
key={`thread-${threadId}`}
onlyTitle
lastItem={activityBlocks.length - 1 === index}
title={
<SystemComment
key={`system-${threadId}`}
pullReqMetadata={pullReqMetadata}
commentItems={commentItems}
repoMetadataPath={repoMetadata.path}
/>
}></ThreadSection>
</Render>
)
}
@ -493,6 +506,8 @@ export const Conversation: React.FC<ConversationProps> = ({
repoMetadata={repoMetadata}
pullRequestMetadata={pullReqMetadata}
refetchReviewers={refetchReviewers}
labels={labels}
refetchLabels={refetchLabels}
/>
</Layout.Horizontal>
</Container>

View File

@ -66,3 +66,10 @@
.iconPadding {
padding-left: 0.6rem !important;
}
.labelsLayout {
width: auto;
flex-wrap: wrap;
justify-content: flex-start;
gap: 7px;
}

View File

@ -18,6 +18,7 @@
// This is an auto-generated file
export declare const alignLayout: string
export declare const iconPadding: string
export declare const labelsLayout: string
export declare const redIcon: string
export declare const reviewerAvatar: string
export declare const reviewerName: string

View File

@ -14,38 +14,44 @@
* limitations under the License.
*/
import React from 'react'
import React, { useState } from 'react'
import { PopoverInteractionKind } from '@blueprintjs/core'
import { useMutate } from 'restful-react'
import { useGet, useMutate } from 'restful-react'
import { omit } from 'lodash-es'
import cx from 'classnames'
import { Container, Layout, Text, Avatar, FlexExpander, useToaster, Utils } from '@harnessio/uicore'
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 } from 'services/code'
import { getErrorMessage } from 'utils/Utils'
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
}
const PullRequestSideBar = (props: PullRequestSideBarProps) => {
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers } = props
// const [searchTerm, setSearchTerm] = useState('')
// const [page] = usePageIndex(1)
const { standalone, hooks } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const [labelQuery, setLabelQuery] = useState<string>('')
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers, labels, refetchLabels } = props
const { getString } = useStrings()
// const tagArr = []
const { showError } = useToaster()
const { showError, showSuccess } = useToaster()
const generateReviewDecisionInfo = (
reviewDecision: EnumPullReqReviewDecision | PullReqReviewDecision.outdated
): {
@ -159,6 +165,27 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
verb: 'DELETE',
path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}`
})
const { mutate: removeLabel } = useMutate({
verb: 'DELETE',
base: getConfig('code/api/v1'),
path: ({ label_id }) =>
`/repos/${encodeURIComponent(repoMetadata.path as string)}/pullreq/${
pullRequestMetadata?.number
}/labels/${encodeURIComponent(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
@ -376,6 +403,71 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
</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}
/>
</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
)
})
.catch(err => {
showError(getErrorMessage(err))
})
refetchLabels()
}}
/>
))
) : (
<Text color={Color.GREY_300} font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
{getString('labels.noLabels')}
</Text>
)}
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Render>
</Container>
</Container>
)

View File

@ -19,16 +19,18 @@ import { Avatar, Container, Layout, StringSubstitute, Text } from '@harnessio/ui
import { Icon, IconName } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { defaultTo } from 'lodash-es'
import { Case, Match } from 'react-jsx-match'
import { CodeIcon, GitInfoProps, MergeStrategy } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import type { TypesPullReqActivity } from 'services/code'
import type { CommentItem } from 'components/CommentBox/CommentBox'
import { PullRequestSection } from 'utils/Utils'
import { CommentType } from 'components/DiffViewer/DiffViewerUtils'
import { CommentType, LabelActivity } from 'components/DiffViewer/DiffViewerUtils'
import { useAppContext } from 'AppContext'
import { CommitActions } from 'components/CommitActions/CommitActions'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { Label } from 'components/Label/Label'
import css from './Conversation.module.scss'
interface SystemCommentProps extends Pick<GitInfoProps, 'pullReqMetadata'> {
@ -41,7 +43,7 @@ interface MergePayload {
merge_method: string
rules_bypassed: boolean
}
//ToDo : update all comment options with the correct payload type and remove Unknown
export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, commentItems, repoMetadataPath }) => {
const { getString } = useStrings()
const payload = commentItems[0].payload
@ -295,6 +297,77 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
)
}
case CommentType.LABEL_MODIFY: {
return (
<Container className={css.mergedBox}>
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
<Avatar name={payload?.author?.display_name} size="small" hoverCard={false} />
<Text tag="div">
<Match expr={(payload?.payload as Unknown).type}>
<Case val={LabelActivity.ASSIGN}>
<strong>{payload?.author?.display_name}</strong> {getString('labels.applied')}
<Label
name={(payload?.payload as Unknown).label}
label_color={(payload?.payload as Unknown).label_color}
label_value={{
name: (payload?.payload as Unknown).value,
color: (payload?.payload as Unknown).value_color
}}
scope={(payload?.payload as Unknown).scope}
/>
<span>{getString('labels.label')}</span>
</Case>
<Case val={LabelActivity.RE_ASSIGN}>
<strong>{payload?.author?.display_name}</strong> <span>{getString('labels.updated')}</span>
<Label
name={(payload?.payload as Unknown).label}
label_color={(payload?.payload as Unknown).label_color}
label_value={{
name: (payload?.payload as Unknown).old_value,
color: (payload?.payload as Unknown).old_value_color
}}
scope={(payload?.payload as Unknown).scope}
/>
<span>{getString('labels.labelTo')}</span>
<Label
name={(payload?.payload as Unknown).label}
label_color={(payload?.payload as Unknown).label_color}
label_value={{
name: (payload?.payload as Unknown).value,
color: (payload?.payload as Unknown).value_color
}}
scope={(payload?.payload as Unknown).scope}
/>
</Case>
<Case val={LabelActivity.UN_ASSIGN}>
<strong>{payload?.author?.display_name}</strong> <span>{getString('labels.removed')}</span>
<Label
name={(payload?.payload as Unknown).label}
label_color={(payload?.payload as Unknown).label_color}
label_value={{
name: (payload?.payload as Unknown).value,
color: (payload?.payload as Unknown).value_color
}}
scope={(payload?.payload as Unknown).scope}
/>
<span>{getString('labels.label')}</span>
</Case>
</Match>
</Text>
<PipeSeparator height={9} />
<TimePopoverWithLocal
time={defaultTo(payload?.created as number, 0)}
inline={true}
width={100}
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
/>
</Layout.Horizontal>
</Container>
)
}
default: {
// eslint-disable-next-line no-console
console.warn('Unable to render system type activity', commentItems)

View File

@ -20,7 +20,7 @@
.table {
.row {
height: 80px;
height: fit-content;
display: flex;
justify-content: center;
padding: 0;
@ -37,6 +37,16 @@
.convo {
white-space: nowrap;
}
.prLabels {
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 10px;
}
.prwrap {
width: 80% !important;
flex-wrap: wrap;
}
}
.rowLink:hover {

View File

@ -19,6 +19,8 @@
export declare const convo: string
export declare const convoIcon: string
export declare const main: string
export declare const prLabels: string
export declare const prwrap: string
export declare const row: string
export declare const rowLink: string
export declare const state: string

View File

@ -15,25 +15,44 @@
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Container, PageBody, Text, TableV2, Layout, StringSubstitute, FlexExpander, Utils } from '@harnessio/uicore'
import {
Container,
PageBody,
Text,
TableV2,
Layout,
StringSubstitute,
FlexExpander,
Utils,
stringSubstitute
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { Link, useHistory } from 'react-router-dom'
import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
import type { CellProps, Column } from 'react-table'
import { Case, Match, Render, Truthy } from 'react-jsx-match'
import { defaultTo, noop } from 'lodash-es'
import { defaultTo, isEmpty, noop } from 'lodash-es'
import { makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { voidFn, getErrorMessage, LIST_FETCHING_LIMIT, permissionProps, PageBrowserProps } from 'utils/Utils'
import {
voidFn,
getErrorMessage,
LIST_FETCHING_LIMIT,
permissionProps,
PageBrowserProps,
ColorName,
LabelFilterObj,
LabelFilterType
} from 'utils/Utils'
import { usePageIndex } from 'hooks/usePageIndex'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { useQueryParams } from 'hooks/useQueryParams'
import type { TypesPullReq, RepoRepositoryOutput } from 'services/code'
import type { TypesPullReq, RepoRepositoryOutput, TypesLabelPullReqAssignmentInfo } from 'services/code'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
@ -42,6 +61,7 @@ import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequ
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import useSpaceSSE from 'hooks/useSpaceSSE'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { Label } from 'components/Label/Label'
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
import css from './PullRequests.module.scss'
@ -50,11 +70,13 @@ const SSE_EVENTS = ['pullreq_updated']
export default function PullRequests() {
const { getString } = useStrings()
const history = useHistory()
const { routes } = useAppContext()
const { routes, hooks, standalone } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const [searchTerm, setSearchTerm] = useState<string | undefined>()
const browserParams = useQueryParams<PageBrowserProps>()
const [filter, setFilter] = useState(browserParams?.state || (PullRequestFilterOption.OPEN as string))
const [authorFilter, setAuthorFilter] = useState<string>()
const [labelFilter, setLabelFilter] = useState<LabelFilterObj[]>([])
const space = useGetSpaceParam()
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
const pageInit = browserParams.page ? parseInt(browserParams.page) : 1
@ -89,7 +111,21 @@ export default function PullRequests() {
order: 'desc',
query: searchTerm,
state: browserParams.state ? browserParams.state : filter == PullRequestFilterOption.ALL ? '' : filter,
...(authorFilter && { created_by: Number(authorFilter) })
...(authorFilter && { created_by: Number(authorFilter) }),
...(labelFilter.filter(({ type, valueId }) => type === 'label' || valueId === -1).length && {
label_id: labelFilter
.filter(({ type, valueId }) => type === 'label' || valueId === -1)
.map(({ labelId }) => labelId)
}),
...(labelFilter.filter(({ type }) => type === 'value').length && {
value_id: labelFilter
.filter(({ type, valueId }) => type === 'value' && valueId !== -1)
.map(({ valueId }) => valueId)
})
},
queryParamStringifyOptions: {
arrayFormat: 'repeat'
},
debounce: 500,
lazy: !repoMetadata
@ -112,8 +148,6 @@ export default function PullRequests() {
onEvent: eventHandler
})
const { standalone } = useAppContext()
const { hooks } = useAppContext()
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
@ -125,6 +159,76 @@ export default function PullRequests() {
[space]
)
const handleLabelClick = (labelFilterArr: LabelFilterObj[], clickedLabel: TypesLabelPullReqAssignmentInfo) => {
// if not present - add :
const isLabelAlreadyAdded = labelFilterArr.map(({ labelId }) => labelId).includes(clickedLabel.id || -1)
const updatedLabelsList = [...labelFilterArr]
if (!isLabelAlreadyAdded && clickedLabel?.id) {
if (clickedLabel.value && clickedLabel.value_id) {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.VALUE,
valueId: clickedLabel.value_id,
labelObj: clickedLabel,
valueObj: {
id: clickedLabel.value_id,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: clickedLabel.value
}
})
} else if (clickedLabel.value_count && !clickedLabel.value_id) {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.VALUE,
valueId: -1,
labelObj: clickedLabel,
valueObj: {
id: -1,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: getString('labels.anyValueOption')
}
})
} else {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.LABEL,
valueId: undefined,
labelObj: clickedLabel,
valueObj: undefined
})
}
setLabelFilter(updatedLabelsList)
}
// if 'any value' label present - replace :
const replacedAnyValueIfPresent = updatedLabelsList.map(filterObj => {
if (
filterObj.valueId === -1 &&
filterObj.labelId === clickedLabel.id &&
clickedLabel.value_id &&
clickedLabel.value
) {
return {
...filterObj,
valueId: clickedLabel.value_id,
valueObj: {
id: clickedLabel.value_id,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: clickedLabel.value
}
}
}
return filterObj
})
const isUpdated = !updatedLabelsList.every((obj, index) => obj === replacedAnyValueIfPresent[index])
if (isUpdated) {
setLabelFilter(replacedAnyValueIfPresent)
}
}
const columns: Column<TypesPullReq>[] = useMemo(
() => [
{
@ -132,32 +236,63 @@ export default function PullRequests() {
width: '100%',
Cell: ({ row }: CellProps<TypesPullReq>) => {
return (
<Link
<Container
className={css.rowLink}
to={routes.toCODEPullRequest({
repoPath: repoMetadata?.path as string,
pullRequestId: String(row.original.number)
})}>
onClick={() =>
history.push(
routes.toCODEPullRequest({
repoPath: repoMetadata?.path as string,
pullRequestId: String(row.original.number)
})
)
}>
<Layout.Horizontal className={css.titleRow} spacing="medium">
<PullRequestStateLabel iconSize={22} data={row.original} iconOnly />
<Container padding={{ left: 'small' }}>
<Layout.Vertical spacing="xsmall">
<Layout.Vertical spacing="small">
<Container>
<Layout.Horizontal>
<Text color={Color.GREY_800} className={css.title} lineClamp={1}>
{row.original.title}
</Text>
<Container className={css.convo}>
<Icon
className={css.convoIcon}
padding={{ left: 'medium', right: 'xsmall' }}
name="code-chat"
size={15}
/>
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_500} tag="span">
{row.original.stats?.conversations}
<Layout.Horizontal flex={{ alignItems: 'center' }} className={css.prLabels}>
<Layout.Horizontal spacing={'xsmall'}>
<Text color={Color.GREY_800} className={css.title} lineClamp={1}>
{row.original.title}
</Text>
</Container>
<Container className={css.convo}>
<Icon
className={css.convoIcon}
padding={{ left: 'small', right: 'small' }}
name="code-chat"
size={15}
/>
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_500} tag="span">
{row.original.stats?.conversations}
</Text>
</Container>
</Layout.Horizontal>
<Render
when={
(isLabelEnabled || standalone) &&
row.original &&
row.original.labels &&
row.original.labels.length !== 0 &&
!prLoading
}>
{row.original?.labels?.map((label, index) => (
<Label
key={index}
name={label.key as string}
label_color={label.color as ColorName}
label_value={{
name: label.value as string,
color: label.value_color as ColorName
}}
scope={label.scope}
onClick={() => {
handleLabelClick(labelFilter, label)
}}
/>
))}
</Render>
</Layout.Horizontal>
</Container>
<Container>
@ -223,7 +358,7 @@ export default function PullRequests() {
{/* TODO: Pass proper state when check api is fully implemented */}
{/* <ExecutionStatusLabel data={{ state: 'success' }} /> */}
</Layout.Horizontal>
</Link>
</Container>
)
}
}
@ -256,12 +391,72 @@ export default function PullRequests() {
setPage(1)
}}
activePullRequestAuthorFilterOption={authorFilter}
activePullRequestLabelFilterOption={labelFilter}
onPullRequestAuthorFilterChanged={_authorFilter => {
setAuthorFilter(_authorFilter)
setPage(1)
}}
onPullRequestLabelFilterChanged={_labelFilter => {
setLabelFilter(_labelFilter)
setPage(1)
}}
/>
<Container padding="xlarge">
<Container padding={{ top: 'medium', bottom: 'large' }}>
<Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
style={{ flexWrap: 'wrap', gap: '5px' }}>
<Render when={!prLoading}>
{isEmpty(data) ? (
<Text color={Color.GREY_400}>{getString('labels.noResults')}</Text>
) : (
<Text color={Color.GREY_400}>
{
stringSubstitute(getString('labels.prCount'), {
count: data?.length
}) as string
}
</Text>
)}
</Render>
{(isLabelEnabled || standalone) &&
labelFilter &&
labelFilter?.length !== 0 &&
labelFilter?.map((label, index) => (
<Label
key={index}
name={label.labelObj.key as string}
label_color={label.labelObj.color as ColorName}
label_value={{
name: label.valueObj?.value as string,
color: label.valueObj?.color as ColorName
}}
scope={label.labelObj.scope}
removeLabelBtn={true}
handleRemoveClick={() => {
if (label.type === 'value') {
const updateFilterObjArr = labelFilter.filter(filterObj => {
if (!(filterObj.labelId === label.labelId && filterObj.type === 'value')) {
return filterObj
}
})
setLabelFilter(updateFilterObjArr)
setPage(1)
} else if (label.type === 'label') {
const updateFilterObjArr = labelFilter.filter(filterObj => {
if (!(filterObj.labelId === label.labelId && filterObj.type === 'label')) {
return filterObj
}
})
setLabelFilter(updateFilterObjArr)
setPage(1)
}
}}
disableRemoveBtnTooltip={true}
/>
))}
</Layout.Horizontal>
</Container>
<Match expr={data?.length}>
<Truthy>
<>
@ -279,6 +474,8 @@ export default function PullRequests() {
<Case val={0}>
<NoResultCard
forSearch={!!searchTerm}
forFilter={!isEmpty(labelFilter) || !isEmpty(authorFilter)}
emptyFilterMessage={getString('pullRequestNotFoundforFilter')}
message={getString('pullRequestEmpty')}
buttonText={getString('newPullRequest')}
onButtonClick={() =>

View File

@ -28,6 +28,7 @@ import {
} from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { sortBy } from 'lodash-es'
import { Render } from 'react-jsx-match'
import { getConfig, getUsingFetch } from 'services/config'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
@ -35,16 +36,19 @@ import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { TypesPrincipalInfo, TypesUser } from 'services/code'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { PageBrowserProps, permissionProps } from 'utils/Utils'
import { LabelFilterObj, PageBrowserProps, permissionProps } from 'utils/Utils'
import { useQueryParams } from 'hooks/useQueryParams'
import { LabelFilter } from 'components/Label/LabelFilter/LabelFilter'
import css from './PullRequestsContentHeader.module.scss'
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
loading?: boolean
activePullRequestFilterOption?: string
activePullRequestAuthorFilterOption?: string
activePullRequestLabelFilterOption?: LabelFilterObj[]
onPullRequestFilterChanged: React.Dispatch<React.SetStateAction<string>>
onPullRequestAuthorFilterChanged: (authorFilter: string) => void
onPullRequestLabelFilterChanged: (labelFilter: LabelFilterObj[]) => void
onSearchTermChanged: (searchTerm: string) => void
}
@ -52,9 +56,11 @@ export function PullRequestsContentHeader({
loading,
onPullRequestFilterChanged,
onPullRequestAuthorFilterChanged,
onPullRequestLabelFilterChanged,
onSearchTermChanged,
activePullRequestFilterOption = PullRequestFilterOption.OPEN,
activePullRequestAuthorFilterOption,
activePullRequestLabelFilterOption,
repoMetadata
}: PullRequestsContentHeaderProps) {
const history = useHistory()
@ -62,11 +68,13 @@ export function PullRequestsContentHeader({
const browserParams = useQueryParams<PageBrowserProps>()
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption)
const [labelFilterOption, setLabelFilterOption] = useState(activePullRequestLabelFilterOption)
const [searchTerm, setSearchTerm] = useState('')
const [query, setQuery] = useState<string>('')
const [loadingAuthors, setLoadingAuthors] = useState<boolean>(false)
const space = useGetSpaceParam()
const { hooks, currentUser, standalone, routingId, routes } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
@ -78,6 +86,10 @@ export function PullRequestsContentHeader({
[space]
)
useEffect(() => {
setLabelFilterOption(activePullRequestLabelFilterOption)
}, [activePullRequestLabelFilterOption])
useEffect(() => {
setFilterOption(browserParams?.state as string)
}, [browserParams])
@ -176,6 +188,16 @@ export function PullRequestsContentHeader({
}}
/>
<FlexExpander />
<Render when={isLabelEnabled || standalone}>
<LabelFilter
labelFilterOption={labelFilterOption}
setLabelFilterOption={setLabelFilterOption}
onPullRequestLabelFilterChanged={onPullRequestLabelFilterChanged}
bearerToken={bearerToken}
repoMetadata={repoMetadata}
spaceRef={space}
/>
</Render>
<DropDown
value={authorFilterOption}
items={() => getAuthorsPromise()}

View File

@ -48,7 +48,7 @@ import Private from '../../../icons/private.svg?url'
import css from '../RepositorySettings.module.scss'
interface GeneralSettingsProps {
repoMetadata: RepoRepositoryOutput | undefined
repoMetadata?: RepoRepositoryOutput
refetch: () => void
gitRef: string
isRepositoryEmpty: boolean

View File

@ -15,7 +15,7 @@
*/
.main {
max-height: calc(var(--page-height) - 160px);
min-height: calc(var(--page-height) - 160px);
background-color: var(--primary-bg) !important;
width: 100%;
margin: var(--spacing-small);
@ -27,7 +27,6 @@
.bp3-tab-panel {
width: 100%;
height: 500px;
}
.bp3-tab {

View File

@ -24,20 +24,24 @@ import { useDisableCodeMainLinks } from 'hooks/useDisableCodeMainLinks'
import { useGetResourceContent } from 'hooks/useGetResourceContent'
import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { getErrorMessage, voidFn } from 'utils/Utils'
import { LabelsPageScope, getErrorMessage, voidFn } from 'utils/Utils'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
// import Webhooks from 'pages/Webhooks/Webhooks'
import { useAppContext } from 'AppContext'
import BranchProtectionListing from 'components/BranchProtection/BranchProtectionListing'
import { SettingsTab, normalizeGitRef } from 'utils/GitUtils'
import SecurityScanSettings from 'pages/RepositorySettings/SecurityScanSettings/SecurityScanSettings'
import LabelsListing from 'pages/Labels/LabelsListing'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import GeneralSettingsContent from './GeneralSettingsContent/GeneralSettingsContent'
import css from './RepositorySettings.module.scss'
export default function RepositorySettings() {
const { repoMetadata, error, loading, refetch, settingSection, gitRef, resourcePath } = useGetRepositoryMetadata()
const space = useGetSpaceParam()
const history = useHistory()
const { routes } = useAppContext()
const { routes, hooks, standalone } = useAppContext()
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
const [activeTab, setActiveTab] = React.useState<string>(settingSection || SettingsTab.general)
const { getString } = useStrings()
const { isRepositoryEmpty } = useGetResourceContent({
@ -71,7 +75,24 @@ export default function RepositorySettings() {
id: SettingsTab.security,
title: getString('security'),
panel: <SecurityScanSettings repoMetadata={repoMetadata} activeTab={activeTab} />
}
},
...(isLabelEnabled || standalone
? [
{
id: SettingsTab.labels,
title: getString('labels.labels'),
panel: (
<LabelsListing
activeTab={activeTab}
repoMetadata={repoMetadata}
currentPageScope={LabelsPageScope.REPOSITORY}
space={space}
/>
)
}
]
: [])
// {
// id: SettingsTab.webhooks,
// title: getString('webhooks'),
@ -87,7 +108,7 @@ export default function RepositorySettings() {
<RepositoryPageHeader
className={css.headerContainer}
repoMetadata={repoMetadata}
title={getString('settings')}
title={getString('manageRepository')}
dataTooltipId="repositorySettings"
/>
<PageBody error={getErrorMessage(error)} retryOnError={voidFn(refetch)}>

View File

@ -40,7 +40,7 @@ import type { RepoRepositoryOutput } from 'services/code'
import { useStrings } from 'framework/strings'
import type { ExportFormDataExtended } from 'utils/GitUtils'
import Upgrade from '../../../icons/Upgrade.svg?url'
import css from '../SpaceSettings.module.scss'
import css from '../GeneralSettings/GeneralSpaceSettings.module.scss'
interface ExportFormProps {
handleSubmit: (data: ExportFormDataExtended) => void

View File

@ -0,0 +1,211 @@
/*
* 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.
*/
.mainCtn {
height: var(--page-height);
background-color: var(--primary-bg) !important;
.roleBadge {
text-transform: capitalize;
padding: var(--spacing-xsmall) 6px;
border-radius: 4px;
border: 1px solid var(--grey-200);
background: var(--grey-50);
width: max-content;
}
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.label {
width: 100px;
padding-right: var(--spacing-medium);
}
.content {
width: 93%;
}
.deleteContainer {
display: flex;
justify-content: space-between;
}
.deleteBtn {
white-space: nowrap !important;
min-width: auto !important;
}
.saveBtn {
margin-left: var(--spacing-medium) !important;
}
.textContainer {
width: 80%;
}
.buttonContainer {
width: 20%;
padding-top: var(--spacing-xsmall) !important;
}
.generalContainer {
width: 100%;
background: var(--grey-0) !important;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
overflow: hidden !important;
position: relative !important;
}
.yellowContainer {
background: var(--orange-50) !important;
padding: var(--spacing-medium) var(--spacing-small);
margin: 0 var(--spacing-large) 0 0 !important;
border-radius: 4px;
}
.verticalContainer {
width: 100% !important;
}
.upgradeContainer {
align-items: center;
overflow: hidden !important;
position: relative !important;
.button {
--background-color: var(--green-700) !important;
--background-color-hover: var(--green-800) !important;
--background-color-active: var(--green-700) !important;
z-index: 2;
.buttonText {
font-size: 13px !important;
}
}
}
.harnessWatermark {
position: absolute;
right: -2.81%;
top: 6%;
bottom: -17%;
z-index: 1;
opacity: 0.1 !important;
svg {
opacity: 0.1;
}
}
.upgradeHeader {
align-items: center;
}
.importContainer {
background: var(--grey-50) !important;
border: 1px solid var(--grey-200) !important;
border-radius: 4px;
:global {
.bp3-form-group {
margin: unset !important;
}
}
}
.detailsLabel {
white-space: nowrap !important;
max-width: 155px !important;
color: var(--grey-600) !important;
}
.icon {
> svg {
fill: var(--primary-7) !important;
> path {
fill: var(--primary-7) !important;
}
}
}
.checkbox {
:global {
[class*='Tooltip--acenter'] {
opacity: 0.7 !important;
}
.bp3-control-indicator {
background: var(--primary-7) !important;
opacity: 0.7;
}
}
}
.repoInfo {
border-radius: 4px;
border: 1px solid var(--grey-100);
background: var(--grey-50) !important;
}
.upgradeButton {
--background-color: var(--green-700) !important;
--background-color-hover: var(--green-800) !important;
--background-color-active: var(--green-700) !important;
z-index: 2;
.buttonText {
font-size: 13px !important;
}
}
.progressBar {
width: 461px;
}
.harnessUpgradeWatermark {
position: absolute;
right: 2%;
top: 6%;
bottom: -17%;
z-index: 1;
opacity: 0.1 !important;
svg {
opacity: 0.1;
}
}
.mainContainer {
:global {
--bp3-intent-color: unset !important;
.bp3-intent-danger {
--bp3-intent-color: unset !important;
}
.bp3-form-helper-text {
margin-top: unset !important;
}
[class*='FormError--errorDiv'] {
display: none !important;
}
}
}
.textSize {
font-size: 13px !important;
}

View File

@ -0,0 +1,46 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const button: string
export declare const buttonContainer: string
export declare const buttonText: string
export declare const checkbox: string
export declare const content: string
export declare const deleteBtn: string
export declare const deleteContainer: string
export declare const detailsLabel: string
export declare const dividerContainer: string
export declare const generalContainer: string
export declare const harnessUpgradeWatermark: string
export declare const harnessWatermark: string
export declare const icon: string
export declare const importContainer: string
export declare const label: string
export declare const mainContainer: string
export declare const mainCtn: string
export declare const progressBar: string
export declare const repoInfo: string
export declare const roleBadge: string
export declare const saveBtn: string
export declare const textContainer: string
export declare const textSize: string
export declare const upgradeButton: string
export declare const upgradeContainer: string
export declare const upgradeHeader: string
export declare const verticalContainer: string
export declare const yellowContainer: string

View File

@ -0,0 +1,521 @@
/*
* 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, { useEffect, useMemo, useState } from 'react'
import {
Button,
Text,
Container,
Formik,
Layout,
Page,
ButtonVariation,
ButtonSize,
FlexExpander,
useToaster,
Heading,
TextInput,
stringSubstitute
} from '@harnessio/uicore'
import cx from 'classnames'
import { noop } from 'lodash-es'
import { useMutate, useGet } from 'restful-react'
import { Intent, Color, FontVariation } from '@harnessio/design-system'
import { useHistory } from 'react-router-dom'
import { Dialog } from '@blueprintjs/core'
import { ProgressBar, Intent as IntentCore } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { JobProgress, useGetSpace } from 'services/code'
import { useAppContext } from 'AppContext'
import { useStrings } from 'framework/strings'
import { REPO_EXPORT_STATE, getErrorMessage } from 'utils/Utils'
import { ACCESS_MODES, permissionProps, voidFn } from 'utils/Utils'
import type { ExportFormDataExtended } from 'utils/GitUtils'
import { useModalHook } from 'hooks/useModalHook'
import useSpaceSSE from 'hooks/useSpaceSSE'
import Harness from '../../../icons/Harness.svg?url'
import Upgrade from '../../../icons/Upgrade.svg?url'
import useDeleteSpaceModal from '../DeleteSpaceModal/DeleteSpaceModal'
import ExportForm from '../ExportForm/ExportForm'
import css from './GeneralSpaceSettings.module.scss'
export default function GeneralSpaceSettings() {
const { space } = useGetRepositoryMetadata()
const { openModal: openDeleteSpaceModal } = useDeleteSpaceModal()
const { data, refetch } = useGetSpace({ space_ref: encodeURIComponent(space), lazy: !space })
const [editName, setEditName] = useState(ACCESS_MODES.VIEW)
const history = useHistory()
const { routes, standalone, hooks } = useAppContext()
const [upgrading, setUpgrading] = useState(false)
const [editDesc, setEditDesc] = useState(ACCESS_MODES.VIEW)
const [repoCount, setRepoCount] = useState(0)
const [exportDone, setExportDone] = useState(false)
const { showError, showSuccess } = useToaster()
const { getString } = useStrings()
const { mutate: patchSpace } = useMutate({
verb: 'PATCH',
path: `/api/v1/spaces/${space}`
})
const { mutate: updateName } = useMutate({
verb: 'POST',
path: `/api/v1/spaces/${space}/move`
})
const { data: exportProgressSpace, refetch: refetchExport } = useGet({
path: `/api/v1/spaces/${space}/export-progress`
})
const countFinishedRepos = (): number => {
return exportProgressSpace?.repos.filter((repo: JobProgress) => repo.state === REPO_EXPORT_STATE.FINISHED).length
}
const checkReposState = () => {
return exportProgressSpace?.repos.every(
(repo: JobProgress) =>
repo.state === REPO_EXPORT_STATE.FINISHED ||
repo.state === REPO_EXPORT_STATE.FAILED ||
repo.state === REPO_EXPORT_STATE.CANCELED
)
}
const checkExportIsRunning = () => {
return exportProgressSpace?.repos.every(
(repo: JobProgress) => repo.state === REPO_EXPORT_STATE.RUNNING || repo.state === REPO_EXPORT_STATE.SCHEDULED
)
}
useEffect(() => {
if (exportProgressSpace?.repos && checkExportIsRunning()) {
setUpgrading(true)
setRepoCount(exportProgressSpace?.repos.length)
setExportDone(false)
} else if (exportProgressSpace?.repos && checkReposState()) {
setRepoCount(countFinishedRepos)
setExportDone(true)
}
}, [exportProgressSpace]) // eslint-disable-line react-hooks/exhaustive-deps
const events = useMemo(() => ['repository_export_completed'], [])
useSpaceSSE({
space,
events,
onEvent: () => {
refetchExport()
if (exportProgressSpace && checkReposState()) {
setRepoCount(countFinishedRepos)
setExportDone(true)
} else if (exportProgressSpace?.repos && checkExportIsRunning()) {
setUpgrading(true)
setRepoCount(exportProgressSpace?.repos.length)
setExportDone(false)
}
}
})
const ExportModal = () => {
const [step, setStep] = useState(0)
const { mutate: exportSpace } = useMutate({
verb: 'POST',
path: `/api/v1/spaces/${space}/export`
})
const handleExportSubmit = (formData: ExportFormDataExtended) => {
try {
setRepoCount(formData.repoCount)
const exportPayload = {
account_id: formData.accountId || '',
org_identifier: formData.organization,
project_identifier: formData.name,
token: formData.token
}
exportSpace(exportPayload)
.then(_ => {
hideModal()
setUpgrading(true)
refetchExport()
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToImportSpace'))
})
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToImportSpace'))
}
}
return (
<Dialog
isOpen
onClose={hideModal}
enforceFocus={false}
title={''}
style={{
width: 610,
maxHeight: '95vh',
overflow: 'auto'
}}>
<Layout.Vertical
padding={{ left: 'xxxlarge' }}
style={{ height: '100%' }}
data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'large' }}>
<Layout.Horizontal className={css.upgradeHeader}>
<img width={30} height={30} src={Harness} />
<Text padding={{ left: 'small' }} font={{ variation: FontVariation.H4 }}>
{step === 0 && <>{getString('exportSpace.upgradeHarness')}</>}
{step === 1 && <>{getString('exportSpace.newProject')}</>}
{step === 2 && <>{getString('exportSpace.upgradeConfirmation')}</>}
</Text>
</Layout.Horizontal>
</Heading>
<Container margin={{ right: 'xlarge' }}>
<ExportForm
hideModal={hideModal}
step={step}
setStep={setStep}
handleSubmit={handleExportSubmit}
loading={false}
space={space}
/>
</Container>
</Layout.Vertical>
</Dialog>
)
}
const [openModal, hideModal] = useModalHook(ExportModal, [noop, space])
const permEditResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY'
},
permissions: ['code_repo_edit']
},
[space]
)
const permDeleteResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY'
},
permissions: ['code_repo_delete']
},
[space]
)
return (
<Container className={css.mainCtn}>
<Page.Body>
<Container padding="xlarge">
<Formik
formName="spaceGeneralSettings"
initialValues={{
name: data?.identifier,
desc: data?.description
}}
onSubmit={voidFn(() => {
// @typescript-eslint/no-empty-function
})}>
{formik => {
return (
<Layout.Vertical padding={{ top: 'medium' }}>
{upgrading ? (
<Container
height={exportDone ? 150 : 187}
color={Color.PRIMARY_BG}
padding="xlarge"
margin={{ bottom: 'medium' }}
className={css.generalContainer}>
<img width={148} height={148} src={Harness} className={css.harnessUpgradeWatermark} />
<Layout.Horizontal className={css.upgradeContainer}>
<img width={24} height={24} src={Harness} color={'blue'} />
<Text
padding={{ left: 'small' }}
font={{ variation: FontVariation.CARD_TITLE, size: 'medium' }}>
{exportDone
? repoCount
? getString('exportSpace.exportCompleted')
: getString('exportSpace.exportFailed')
: getString('exportSpace.upgradeProgress')}
</Text>
</Layout.Horizontal>
<Container padding={'xxlarge'}>
<Layout.Vertical spacing="large">
{exportDone ? null : <ProgressBar intent={IntentCore.PRIMARY} className={css.progressBar} />}
<Container padding={{ top: 'small' }}>
{exportDone ? (
<Text
icon={repoCount ? 'execution-success' : 'cross'}
iconProps={{
size: 16,
color: repoCount ? Color.GREEN_500 : Color.RED_500
}}>
<Text padding={{ left: 'small' }}>
{repoCount
? (stringSubstitute(getString('exportSpace.exportRepoCompleted'), {
repoCount
}) as string)
: getString('exportSpace.upgradeFailed')}
{!repoCount && (
<a target="_blank" rel="noreferrer" href="https://docs.gitness.com/support">
<Icon className={css.icon} name="code-info" size={16} />
</a>
)}
</Text>
</Text>
) : (
<Text
icon={'steps-spinner'}
iconProps={{
size: 16,
color: Color.GREY_300
}}>
<Text padding={{ left: 'small' }}>
{
stringSubstitute(getString('exportSpace.exportRepo'), {
repoCount
}) as string
}
</Text>
</Text>
)}
</Container>
</Layout.Vertical>
</Container>
</Container>
) : (
<Container
color={Color.PRIMARY_BG}
padding="xlarge"
margin={{ bottom: 'medium' }}
className={css.generalContainer}>
<img width={148} height={148} src={Harness} className={css.harnessWatermark} />
<Layout.Horizontal className={css.upgradeContainer}>
<img width={24} height={24} src={Harness} color={'blue'} />
<Text
padding={{ left: 'small' }}
font={{ variation: FontVariation.CARD_TITLE, size: 'medium' }}>
{getString('exportSpace.upgradeTitle')}
</Text>
<FlexExpander />
<Button
className={css.button}
variation={ButtonVariation.PRIMARY}
onClick={() => {
openModal()
}}
text={
<Layout.Horizontal
onClick={() => {
openModal()
}}>
<img width={16} height={16} src={Upgrade} />
<Text className={css.buttonText} color={Color.GREY_0}>
{getString('exportSpace.upgrade')}
</Text>
</Layout.Horizontal>
}
intent="success"
size={ButtonSize.MEDIUM}
/>
</Layout.Horizontal>
<Text padding={{ top: 'large', left: 'xlarge' }} color={Color.GREY_500} font={{ size: 'small' }}>
{getString('exportSpace.upgradeContent')}
<a target="_blank" rel="noreferrer" href="https://developer.harness.io/docs/code-repository">
<Icon className={css.icon} name="code-info" size={16} />
</a>
</Text>
</Container>
)}
<Container padding="xlarge" margin={{ bottom: 'medium' }} className={css.generalContainer}>
<Layout.Horizontal padding={{ bottom: 'medium' }}>
<Container className={css.label}>
<Text padding={{ top: 'small' }} color={Color.GREY_600} className={css.textSize}>
{getString('name')}
</Text>
</Container>
<Container className={css.content}>
{editName === ACCESS_MODES.EDIT ? (
<Layout.Horizontal>
<TextInput
name="name"
value={formik.values.name || data?.identifier}
className={cx(css.textContainer, css.textSize)}
onChange={evt => {
formik.setFieldValue('name', (evt.currentTarget as HTMLInputElement)?.value)
}}
/>
<Layout.Horizontal className={css.buttonContainer}>
<Button
className={css.saveBtn}
margin={{ right: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
onClick={() => {
updateName({ uid: formik.values?.name })
.then(() => {
showSuccess(getString('spaceUpdate'))
history.push(routes.toCODESpaceSettings({ space: formik.values?.name as string }))
setEditName(ACCESS_MODES.VIEW)
})
.catch(err => {
showError(getErrorMessage(err))
})
}}
/>
<Button
text={getString('cancel')}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
onClick={() => {
formik.setFieldValue('name', data?.identifier)
setEditName(ACCESS_MODES.VIEW)
}}
/>
</Layout.Horizontal>
</Layout.Horizontal>
) : (
<Text color={Color.GREY_800} className={css.textSize}>
{formik?.values?.name || data?.identifier}
<Button
className={css.textSize}
text={getString('edit')}
icon="Edit"
variation={ButtonVariation.LINK}
onClick={() => {
setEditName(ACCESS_MODES.EDIT)
}}
{...permissionProps(permEditResult, standalone)}
/>
</Text>
)}
</Container>
</Layout.Horizontal>
<Layout.Horizontal>
<Container className={css.label}>
<Text padding={{ top: 'small' }} color={Color.GREY_600} className={css.textSize}>
{getString('description')}
</Text>
</Container>
<Container className={css.content}>
{editDesc === ACCESS_MODES.EDIT ? (
<Layout.Horizontal>
<TextInput
onChange={evt => {
formik.setFieldValue('desc', (evt.currentTarget as HTMLInputElement)?.value)
}}
value={formik.values.desc || data?.description}
name="desc"
className={cx(css.textContainer, css.textSize)}
/>
<Layout.Horizontal className={css.buttonContainer}>
<Button
className={cx(css.saveBtn, css.textSize)}
margin={{ right: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
onClick={() => {
patchSpace({ description: formik.values?.desc })
.then(() => {
showSuccess(getString('spaceUpdate'))
setEditDesc(ACCESS_MODES.VIEW)
refetch()
})
.catch(err => {
showError(getErrorMessage(err))
})
}}
/>
<Button
text={getString('cancel')}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
onClick={() => {
formik.setFieldValue('desc', data?.description)
setEditDesc(ACCESS_MODES.VIEW)
}}
/>
</Layout.Horizontal>
</Layout.Horizontal>
) : (
<Text className={css.textSize} color={Color.GREY_800}>
{formik?.values?.desc || data?.description}
<Button
className={css.textSize}
text={getString('edit')}
icon="Edit"
variation={ButtonVariation.LINK}
onClick={() => {
setEditDesc(ACCESS_MODES.EDIT)
}}
{...permissionProps(permEditResult, standalone)}
/>
</Text>
)}
</Container>
</Layout.Horizontal>
</Container>
<Container padding="large" className={css.generalContainer}>
<Container className={css.deleteContainer}>
<Layout.Vertical className={css.verticalContainer}>
<Text icon="main-trash" color={Color.GREY_600} font={{ size: 'small' }}>
{getString('dangerDeleteRepo')}
</Text>
<Layout.Horizontal
padding={{ top: 'medium', left: 'medium' }}
flex={{ justifyContent: 'space-between' }}>
<Container className={css.yellowContainer}>
<Text
icon="main-issue"
iconProps={{ size: 16, color: Color.ORANGE_700, margin: { right: 'small' } }}
padding={{ left: 'large', right: 'large', top: 'small', bottom: 'small' }}
color={Color.WARNING}>
{getString('spaceSetting.intentText', {
space: data?.identifier
})}
</Text>
</Container>
<Button
className={css.deleteBtn}
margin={{ right: 'medium' }}
disabled={false}
intent={Intent.DANGER}
onClick={() => {
openDeleteSpaceModal()
}}
variation={ButtonVariation.SECONDARY}
text={getString('deleteSpace')}
{...permissionProps(permDeleteResult, standalone)}></Button>
</Layout.Horizontal>
</Layout.Vertical>
</Container>
</Container>
</Layout.Vertical>
)
}}
</Formik>
</Container>
</Page.Body>
</Container>
)
}

View File

@ -14,20 +14,95 @@
* limitations under the License.
*/
.mainCtn {
height: var(--page-height);
.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;
}
.roleBadge {
text-transform: capitalize;
padding: var(--spacing-xsmall) 6px;
border-radius: 4px;
border: 1px solid var(--grey-200);
background: var(--grey-50);
width: max-content;
.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;
}
}
}
.webhooksContent {
width: 100%;
}
.btn {
margin-top: 5px;
}
.webhookHeader {
padding-left: 0 !important;
margin-left: 0 !important;
}
.contentContainer {
margin: 20px !important;
}
.generalContainer {
width: 100%;
background: var(--grey-0) !important;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
}
.label {
padding-top: var(--spacing-xsmall);
width: 220px;
padding-right: var(--spacing-medium);
}
.descText {
align-self: center !important;
}
.radioContainer {
:global([class*='RadioButton--radio']) {
align-items: center;
}
}
.iconContainer {
display: flex;
align-items: center;
}
.content {
width: 80%;
}
.deleteContainer {
display: flex;
justify-content: space-between;
}
.text {
text-transform: capitalize;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
@ -35,23 +110,10 @@
margin: 20px 0;
}
.label {
width: 100px;
padding-right: var(--spacing-medium);
}
.content {
width: 93%;
}
.deleteContainer {
display: flex;
justify-content: space-between;
}
.deleteBtn {
white-space: nowrap !important;
min-width: auto !important;
.dialogContainer {
:global(.bp3-dialog-header) {
margin-bottom: var(--spacing-medium) !important;
}
}
.saveBtn {
@ -59,153 +121,87 @@
}
.textContainer {
width: 80%;
width: 100%;
}
.editContainer {
width: 60% !important;
textarea {
min-height: 100px !important;
}
}
.buttonContainer {
width: 20%;
padding-top: var(--spacing-xsmall) !important;
}
.generalContainer {
width: 100%;
background: var(--grey-0) !important;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
border-radius: 4px;
overflow: hidden !important;
position: relative !important;
}
.yellowContainer {
background: var(--orange-50) !important;
padding: var(--spacing-medium) var(--spacing-small);
margin: 0 var(--spacing-large) 0 0 !important;
border-radius: 4px;
}
.verticalContainer {
width: 100% !important;
}
.upgradeContainer {
align-items: center;
overflow: hidden !important;
position: relative !important;
.button {
--background-color: var(--green-700) !important;
--background-color-hover: var(--green-800) !important;
--background-color-active: var(--green-700) !important;
z-index: 2;
.buttonText {
font-size: 13px !important;
}
}
}
.harnessWatermark {
position: absolute;
right: -2.81%;
top: 6%;
bottom: -17%;
z-index: 1;
opacity: 0.1 !important;
svg {
opacity: 0.1;
}
}
.upgradeHeader {
align-items: center;
}
.importContainer {
background: var(--grey-50) !important;
border: 1px solid var(--grey-200) !important;
border-radius: 4px;
:global {
.bp3-form-group {
margin: unset !important;
}
}
}
.detailsLabel {
white-space: nowrap !important;
max-width: 155px !important;
color: var(--grey-600) !important;
}
.icon {
> svg {
fill: var(--primary-7) !important;
> path {
fill: var(--primary-7) !important;
}
}
}
.checkbox {
:global {
[class*='Tooltip--acenter'] {
opacity: 0.7 !important;
}
.bp3-control-indicator {
background: var(--primary-7) !important;
opacity: 0.7;
}
}
}
.repoInfo {
border-radius: 4px;
border: 1px solid var(--grey-100);
background: var(--grey-50) !important;
}
.upgradeButton {
--background-color: var(--green-700) !important;
--background-color-hover: var(--green-800) !important;
--background-color-active: var(--green-700) !important;
z-index: 2;
.buttonText {
font-size: 13px !important;
}
}
.progressBar {
width: 461px;
}
.harnessUpgradeWatermark {
position: absolute;
right: 2%;
top: 6%;
bottom: -17%;
z-index: 1;
opacity: 0.1 !important;
svg {
opacity: 0.1;
}
}
.mainContainer {
:global {
--bp3-intent-color: unset !important;
.bp3-intent-danger {
--bp3-intent-color: unset !important;
}
.bp3-form-helper-text {
margin-top: unset !important;
}
[class*='FormError--errorDiv'] {
display: none !important;
}
}
gap: 10px;
margin-left: auto !important;
}
.textSize {
font-size: 13px !important;
}
.description {
width: 80%;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
margin: 0 auto;
}
.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;
}
}
.headerContainer {
border-bottom: unset !important;
}

View File

@ -16,31 +16,27 @@
/* eslint-disable */
// This is an auto-generated file
export declare const button: string
export declare const btn: string
export declare const buttonContainer: string
export declare const buttonText: string
export declare const checkbox: string
export declare const content: string
export declare const deleteBtn: string
export declare const contentContainer: string
export declare const deleteContainer: string
export declare const detailsLabel: string
export declare const description: string
export declare const descText: string
export declare const dialogContainer: string
export declare const dividerContainer: string
export declare const editContainer: string
export declare const generalContainer: string
export declare const harnessUpgradeWatermark: string
export declare const harnessWatermark: string
export declare const icon: string
export declare const importContainer: string
export declare const headerContainer: string
export declare const iconContainer: string
export declare const label: string
export declare const mainContainer: string
export declare const mainCtn: string
export declare const progressBar: string
export declare const repoInfo: string
export declare const roleBadge: string
export declare const main: string
export declare const radioContainer: string
export declare const saveBtn: string
export declare const tabsContainer: string
export declare const tabTitle: string
export declare const text: string
export declare const textContainer: string
export declare const textSize: string
export declare const upgradeButton: string
export declare const upgradeContainer: string
export declare const upgradeHeader: string
export declare const verticalContainer: string
export declare const yellowContainer: string
export declare const webhookHeader: string
export declare const webhooksContent: string

View File

@ -14,507 +14,67 @@
* limitations under the License.
*/
import React, { useEffect, useMemo, useState } from 'react'
import {
Button,
Text,
Container,
Formik,
Layout,
Page,
ButtonVariation,
ButtonSize,
FlexExpander,
useToaster,
Heading,
TextInput,
stringSubstitute
} from '@harnessio/uicore'
import React from 'react'
import cx from 'classnames'
import { noop } from 'lodash-es'
import { useMutate, useGet } from 'restful-react'
import { Intent, Color, FontVariation } from '@harnessio/design-system'
import { PageBody, Container, Tabs, Page } from '@harnessio/uicore'
import { useHistory } from 'react-router-dom'
import { Dialog } from '@blueprintjs/core'
import { ProgressBar, Intent as IntentCore } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { JobProgress, useGetSpace } from 'services/code'
import { useAppContext } from 'AppContext'
import { useStrings } from 'framework/strings'
import { getErrorMessage } from 'utils/Utils'
import { ACCESS_MODES, permissionProps, voidFn } from 'utils/Utils'
import type { ExportFormDataExtended } from 'utils/GitUtils'
import { useModalHook } from 'hooks/useModalHook'
import useSpaceSSE from 'hooks/useSpaceSSE'
import Harness from '../../icons/Harness.svg?url'
import Upgrade from '../../icons/Upgrade.svg?url'
import useDeleteSpaceModal from './DeleteSpaceModal/DeleteSpaceModal'
import ExportForm from './ExportForm/ExportForm'
import { useAppContext } from 'AppContext'
import { SettingsTab, SpaceSettingsTab } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import LabelsListing from 'pages/Labels/LabelsListing'
import { LabelsPageScope } from 'utils/Utils'
import GeneralSpaceSettings from './GeneralSettings/GeneralSpaceSettings'
import css from './SpaceSettings.module.scss'
export default function SpaceSettings() {
const { space } = useGetRepositoryMetadata()
const { openModal: openDeleteSpaceModal } = useDeleteSpaceModal()
const { data, refetch } = useGetSpace({ space_ref: encodeURIComponent(space), lazy: !space })
const [editName, setEditName] = useState(ACCESS_MODES.VIEW)
const { settingSection } = useGetRepositoryMetadata()
const history = useHistory()
const { routes, standalone, hooks } = useAppContext()
//check upgrading for space
const [upgrading, setUpgrading] = useState(false)
const [editDesc, setEditDesc] = useState(ACCESS_MODES.VIEW)
const [repoCount, setRepoCount] = useState(0)
const [exportDone, setExportDone] = useState(false)
const { showError, showSuccess } = useToaster()
const { routes } = useAppContext()
const space = useGetSpaceParam()
const [activeTab, setActiveTab] = React.useState<string>(settingSection || SpaceSettingsTab.general)
const { getString } = useStrings()
const { mutate: patchSpace } = useMutate({
verb: 'PATCH',
path: `/api/v1/spaces/${space}`
})
const { mutate: updateName } = useMutate({
verb: 'POST',
path: `/api/v1/spaces/${space}/move`
})
const { data: exportProgressSpace, refetch: refetchExport } = useGet({
path: `/api/v1/spaces/${space}/export-progress`
})
const countFinishedRepos = (): number => {
return exportProgressSpace?.repos.filter((repo: JobProgress) => repo.state === 'finished').length
}
const checkReposState = () => {
return exportProgressSpace?.repos.every(
(repo: JobProgress) => repo.state === 'finished' || repo.state === 'failed' || repo.state === 'canceled'
)
}
const checkExportIsRunning = () => {
return exportProgressSpace?.repos.every(
(repo: JobProgress) => repo.state === 'running' || repo.state === 'scheduled'
)
}
useEffect(() => {
if (exportProgressSpace?.repos && checkExportIsRunning()) {
setUpgrading(true)
setRepoCount(exportProgressSpace?.repos.length)
setExportDone(false)
} else if (exportProgressSpace?.repos && checkReposState()) {
setRepoCount(countFinishedRepos)
setExportDone(true)
}
}, [exportProgressSpace]) // eslint-disable-line react-hooks/exhaustive-deps
const events = useMemo(() => ['repository_export_completed'], [])
useSpaceSSE({
space,
events,
onEvent: () => {
refetchExport()
if (exportProgressSpace && checkReposState()) {
setRepoCount(countFinishedRepos)
setExportDone(true)
} else if (exportProgressSpace?.repos && checkExportIsRunning()) {
setUpgrading(true)
setRepoCount(exportProgressSpace?.repos.length)
setExportDone(false)
}
}
})
const ExportModal = () => {
const [step, setStep] = useState(0)
const { mutate: exportSpace } = useMutate({
verb: 'POST',
path: `/api/v1/spaces/${space}/export`
})
const handleExportSubmit = (formData: ExportFormDataExtended) => {
try {
setRepoCount(formData.repoCount)
const exportPayload = {
account_id: formData.accountId || '',
org_identifier: formData.organization,
project_identifier: formData.name,
token: formData.token
}
exportSpace(exportPayload)
.then(_ => {
hideModal()
setUpgrading(true)
refetchExport()
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToImportSpace'))
})
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToImportSpace'))
}
}
return (
<Dialog
isOpen
onClose={hideModal}
enforceFocus={false}
title={''}
style={{
width: 610,
maxHeight: '95vh',
overflow: 'auto'
}}>
<Layout.Vertical
padding={{ left: 'xxxlarge' }}
style={{ height: '100%' }}
data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'large' }}>
<Layout.Horizontal className={css.upgradeHeader}>
<img width={30} height={30} src={Harness} />
<Text padding={{ left: 'small' }} font={{ variation: FontVariation.H4 }}>
{step === 0 && <>{getString('exportSpace.upgradeHarness')}</>}
{step === 1 && <>{getString('exportSpace.newProject')}</>}
{step === 2 && <>{getString('exportSpace.upgradeConfirmation')}</>}
</Text>
</Layout.Horizontal>
</Heading>
<Container margin={{ right: 'xlarge' }}>
<ExportForm
hideModal={hideModal}
step={step}
setStep={setStep}
handleSubmit={handleExportSubmit}
loading={false}
space={space}
/>
</Container>
</Layout.Vertical>
</Dialog>
)
}
const [openModal, hideModal] = useModalHook(ExportModal, [noop, space])
const permEditResult = hooks?.usePermissionTranslate?.(
const tabListArray = [
{
resource: {
resourceType: 'CODE_REPOSITORY'
},
permissions: ['code_repo_edit']
id: SettingsTab.general,
title: 'General',
panel: (
<Container padding={'large'}>
<GeneralSpaceSettings />
</Container>
)
},
[space]
)
const permDeleteResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY'
},
permissions: ['code_repo_delete']
},
[space]
)
id: SettingsTab.labels,
title: getString('labels.labels'),
panel: <LabelsListing activeTab={activeTab} space={space} currentPageScope={LabelsPageScope.SPACE} />
}
]
return (
<Container className={css.mainCtn}>
<Container className={css.main}>
<Page.Header title={getString('spaceSetting.settings')} />
<Page.Body>
<Container padding="xlarge">
<Formik
formName="spaceGeneralSettings"
initialValues={{
name: data?.identifier,
desc: data?.description
}}
onSubmit={voidFn(() => {
// @typescript-eslint/no-empty-function
})}>
{formik => {
return (
<Layout.Vertical padding={{ top: 'medium' }}>
{upgrading ? (
<Container
height={exportDone ? 150 : 187}
color={Color.PRIMARY_BG}
padding="xlarge"
margin={{ bottom: 'medium' }}
className={css.generalContainer}>
<img width={148} height={148} src={Harness} className={css.harnessUpgradeWatermark} />
<Layout.Horizontal className={css.upgradeContainer}>
<img width={24} height={24} src={Harness} color={'blue'} />
<Text
padding={{ left: 'small' }}
font={{ variation: FontVariation.CARD_TITLE, size: 'medium' }}>
{exportDone
? repoCount
? getString('exportSpace.exportCompleted')
: getString('exportSpace.exportFailed')
: getString('exportSpace.upgradeProgress')}
</Text>
</Layout.Horizontal>
<Container padding={'xxlarge'}>
<Layout.Vertical spacing="large">
{exportDone ? null : <ProgressBar intent={IntentCore.PRIMARY} className={css.progressBar} />}
<Container padding={{ top: 'small' }}>
{exportDone ? (
<Text
icon={repoCount ? 'execution-success' : 'cross'}
iconProps={{
size: 16,
color: repoCount ? Color.GREEN_500 : Color.RED_500
}}>
<Text padding={{ left: 'small' }}>
{repoCount
? (stringSubstitute(getString('exportSpace.exportRepoCompleted'), {
repoCount
}) as string)
: getString('exportSpace.upgradeFailed')}
{!repoCount && (
<a target="_blank" rel="noreferrer" href="https://docs.gitness.com/support">
<Icon className={css.icon} name="code-info" size={16} />
</a>
)}
</Text>
</Text>
) : (
<Text
icon={'steps-spinner'}
iconProps={{
size: 16,
color: Color.GREY_300
}}>
<Text padding={{ left: 'small' }}>
{
stringSubstitute(getString('exportSpace.exportRepo'), {
repoCount
}) as string
}
</Text>
</Text>
)}
</Container>
</Layout.Vertical>
</Container>
</Container>
) : (
<Container
color={Color.PRIMARY_BG}
padding="xlarge"
margin={{ bottom: 'medium' }}
className={css.generalContainer}>
<img width={148} height={148} src={Harness} className={css.harnessWatermark} />
<Layout.Horizontal className={css.upgradeContainer}>
<img width={24} height={24} src={Harness} color={'blue'} />
<Text
padding={{ left: 'small' }}
font={{ variation: FontVariation.CARD_TITLE, size: 'medium' }}>
{getString('exportSpace.upgradeTitle')}
</Text>
<FlexExpander />
<Button
className={css.button}
variation={ButtonVariation.PRIMARY}
onClick={() => {
openModal()
}}
text={
<Layout.Horizontal
onClick={() => {
openModal()
}}>
<img width={16} height={16} src={Upgrade} />
<Text className={css.buttonText} color={Color.GREY_0}>
{getString('exportSpace.upgrade')}
</Text>
</Layout.Horizontal>
}
intent="success"
size={ButtonSize.MEDIUM}
/>
</Layout.Horizontal>
<Text padding={{ top: 'large', left: 'xlarge' }} color={Color.GREY_500} font={{ size: 'small' }}>
{getString('exportSpace.upgradeContent')}
<a target="_blank" rel="noreferrer" href="https://developer.harness.io/docs/code-repository">
<Icon className={css.icon} name="code-info" size={16} />
</a>
</Text>
</Container>
)}
<Container padding="xlarge" margin={{ bottom: 'medium' }} className={css.generalContainer}>
<Layout.Horizontal padding={{ bottom: 'medium' }}>
<Container className={css.label}>
<Text padding={{ top: 'small' }} color={Color.GREY_600} className={css.textSize}>
{getString('name')}
</Text>
</Container>
<Container className={css.content}>
{editName === ACCESS_MODES.EDIT ? (
<Layout.Horizontal>
<TextInput
name="name"
value={formik.values.name || data?.identifier}
className={cx(css.textContainer, css.textSize)}
onChange={evt => {
formik.setFieldValue('name', (evt.currentTarget as HTMLInputElement)?.value)
}}
/>
<Layout.Horizontal className={css.buttonContainer}>
<Button
className={css.saveBtn}
margin={{ right: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
onClick={() => {
updateName({ uid: formik.values?.name })
.then(() => {
showSuccess(getString('spaceUpdate'))
history.push(routes.toCODESpaceSettings({ space: formik.values?.name as string }))
setEditName(ACCESS_MODES.VIEW)
})
.catch(err => {
showError(getErrorMessage(err))
})
}}
/>
<Button
text={getString('cancel')}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
onClick={() => {
formik.setFieldValue('name', data?.identifier)
setEditName(ACCESS_MODES.VIEW)
}}
/>
</Layout.Horizontal>
</Layout.Horizontal>
) : (
<Text color={Color.GREY_800} className={css.textSize}>
{formik?.values?.name || data?.identifier}
<Button
className={css.textSize}
text={getString('edit')}
icon="Edit"
variation={ButtonVariation.LINK}
onClick={() => {
setEditName(ACCESS_MODES.EDIT)
}}
{...permissionProps(permEditResult, standalone)}
/>
</Text>
)}
</Container>
</Layout.Horizontal>
<Layout.Horizontal>
<Container className={css.label}>
<Text padding={{ top: 'small' }} color={Color.GREY_600} className={css.textSize}>
{getString('description')}
</Text>
</Container>
<Container className={css.content}>
{editDesc === ACCESS_MODES.EDIT ? (
<Layout.Horizontal>
<TextInput
onChange={evt => {
formik.setFieldValue('desc', (evt.currentTarget as HTMLInputElement)?.value)
}}
value={formik.values.desc || data?.description}
name="desc"
className={cx(css.textContainer, css.textSize)}
/>
<Layout.Horizontal className={css.buttonContainer}>
<Button
className={cx(css.saveBtn, css.textSize)}
margin={{ right: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
onClick={() => {
patchSpace({ description: formik.values?.desc })
.then(() => {
showSuccess(getString('spaceUpdate'))
setEditDesc(ACCESS_MODES.VIEW)
refetch()
})
.catch(err => {
showError(getErrorMessage(err))
})
}}
/>
<Button
text={getString('cancel')}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
onClick={() => {
formik.setFieldValue('desc', data?.description)
setEditDesc(ACCESS_MODES.VIEW)
}}
/>
</Layout.Horizontal>
</Layout.Horizontal>
) : (
<Text className={css.textSize} color={Color.GREY_800}>
{formik?.values?.desc || data?.description}
<Button
className={css.textSize}
text={getString('edit')}
icon="Edit"
variation={ButtonVariation.LINK}
onClick={() => {
setEditDesc(ACCESS_MODES.EDIT)
}}
{...permissionProps(permEditResult, standalone)}
/>
</Text>
)}
</Container>
</Layout.Horizontal>
</Container>
<Container padding="large" className={css.generalContainer}>
<Container className={css.deleteContainer}>
<Layout.Vertical className={css.verticalContainer}>
<Text icon="main-trash" color={Color.GREY_600} font={{ size: 'small' }}>
{getString('dangerDeleteRepo')}
</Text>
<Layout.Horizontal
padding={{ top: 'medium', left: 'medium' }}
flex={{ justifyContent: 'space-between' }}>
<Container className={css.yellowContainer}>
<Text
icon="main-issue"
iconProps={{ size: 16, color: Color.ORANGE_700, margin: { right: 'small' } }}
padding={{ left: 'large', right: 'large', top: 'small', bottom: 'small' }}
color={Color.WARNING}>
{getString('spaceSetting.intentText', {
space: data?.identifier
})}
</Text>
</Container>
<Button
className={css.deleteBtn}
margin={{ right: 'medium' }}
disabled={false}
intent={Intent.DANGER}
onClick={() => {
openDeleteSpaceModal()
}}
variation={ButtonVariation.SECONDARY}
text={getString('deleteSpace')}
{...permissionProps(permDeleteResult, standalone)}></Button>
</Layout.Horizontal>
</Layout.Vertical>
</Container>
</Container>
</Layout.Vertical>
<PageBody>
<Container className={cx(css.main, css.tabsContainer)}>
<Tabs
id="SpaceSettingsTabs"
large={false}
defaultSelectedTabId={activeTab}
animate={false}
onChange={(id: string) => {
setActiveTab(id)
history.replace(
routes.toCODESpaceSettings({
space: space as string,
settingSection: id !== SpaceSettingsTab.general ? (id as string) : ''
})
)
}}
</Formik>
tabList={tabListArray}></Tabs>
</Container>
</Page.Body>
</PageBody>
</Container>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -92,6 +92,14 @@ export const getUsingFetch = <
return res.text().then(text => Promise.reject(text))
}
if (res.status === 504) {
return res.text().then(text => Promise.reject(text))
}
if (res.status === 404) {
return res.text().then(text => Promise.reject(text))
}
return res.text()
})
}

View File

@ -124,7 +124,13 @@ export enum SettingsTab {
webhooks = 'webhook',
general = '/',
branchProtection = 'rules',
security = 'security'
security = 'security',
labels = 'labels'
}
export enum SpaceSettingsTab {
general = '/',
labels = 'labels'
}
export enum VulnerabilityScanningType {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Intent, IToaster, IToastProps, Position, Toaster } from '@blueprintjs/core'
import { IconName, Intent, IToaster, IToastProps, Position, Toaster } from '@blueprintjs/core'
import { get } from 'lodash-es'
import moment from 'moment'
import langMap from 'lang-map'
@ -28,7 +28,10 @@ import type {
TypesRuleViolations,
TypesViolation,
TypesCodeOwnerEvaluationEntry,
TypesListCommitResponse
TypesListCommitResponse,
RepoRepositoryOutput,
TypesLabel,
TypesLabelValue
} from 'services/code'
import type { GitInfoProps } from './GitUtils'
@ -49,6 +52,14 @@ export enum FeatureType {
RELEASED = 'released'
}
export enum REPO_EXPORT_STATE {
FINISHED = 'finished',
FAILED = 'failed',
CANCELED = 'canceled',
RUNNING = 'running',
SCHEDULED = 'scheduled'
}
export const LIST_FETCHING_LIMIT = 20
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
export const DEFAULT_BRANCH_NAME = 'main'
@ -747,3 +758,122 @@ export function removeSpecificTextOptimized(
viewRef?.current?.dispatch({ changes })
}
}
export const enum LabelType {
DYNAMIC = 'dynamic',
STATIC = 'static'
}
export enum ColorName {
Red = 'red',
Green = 'green',
Yellow = 'yellow',
Blue = 'blue',
Pink = 'pink',
Purple = 'purple',
Violet = 'violet',
Indigo = 'indigo',
Cyan = 'cyan',
Orange = 'orange',
Brown = 'brown',
Mint = 'mint',
Lime = 'lime'
}
export interface ColorDetails {
stroke: string
background: string
text: string
backgroundWithoutStroke: string
}
export const colorsPanel: Record<ColorName, ColorDetails> = {
[ColorName.Red]: { background: '#FFF7F7', stroke: '#F3A9AA', text: '#C7292F', backgroundWithoutStroke: '#FFE8EB' },
[ColorName.Green]: { background: '#E9F9EE', stroke: '#85CBA2', text: '#16794C', backgroundWithoutStroke: '#E8F9ED' },
[ColorName.Yellow]: { background: '#FFF9ED', stroke: '#E4B86F', text: '#92582D', backgroundWithoutStroke: '#FFF3D1' },
[ColorName.Blue]: { background: '#F1FCFF', stroke: '#7BC8E0', text: '#236E93', backgroundWithoutStroke: '#E0F9FF' },
[ColorName.Pink]: { background: '#FFF7FC', stroke: '#ECA8D2', text: '#C41B87', backgroundWithoutStroke: '#FEECF7' },
[ColorName.Purple]: { background: '#FFF8FF', stroke: '#DFAAE3', text: '#9C2AAD', backgroundWithoutStroke: '#FCEDFC' },
[ColorName.Violet]: { background: '#FBFAFF', stroke: '#C1B4F3', text: '#5645AF', backgroundWithoutStroke: '#F3F0FF' },
[ColorName.Indigo]: { background: '#F8FAFF', stroke: '#A9BDF5', text: '#3250B2', backgroundWithoutStroke: '#EDF2FF' },
[ColorName.Cyan]: { background: '#F2FCFD', stroke: '#7DCBD9', text: '#0B7792', backgroundWithoutStroke: '#E4F9FB' },
[ColorName.Orange]: { background: '#FFF8F4', stroke: '#FFA778', text: '#995137', backgroundWithoutStroke: '#FFEDD5' },
[ColorName.Brown]: { background: '#FCF9F6', stroke: '#DBB491', text: '#805C43', backgroundWithoutStroke: '#F8EFE7' },
[ColorName.Mint]: { background: '#EFFEF9', stroke: '#7FD0BD', text: '#247469', backgroundWithoutStroke: '#DDFBF3' },
[ColorName.Lime]: { background: '#F7FCF0', stroke: '#AFC978', text: '#586729', backgroundWithoutStroke: '#EDFADA' }
// Add more colors when required
}
export const getColorsObj = (colorKey: ColorName): ColorDetails => {
return colorsPanel[colorKey]
}
export const getScopeIcon = (scope: any, standalone: boolean) => {
if (scope === 0) return undefined
if (scope === 1 && standalone) return 'nav-project' as IconName
if (scope === 1 && !standalone) return 'Account' as IconName
if (scope === 2) return 'nav-organization' as IconName
if (scope === 3) return 'nav-project' as IconName
return undefined
}
export function customEncodeURIComponent(str: string) {
return encodeURIComponent(str).replace(/!/g, '%21')
}
export interface LabelTypes extends TypesLabel {
labelValues?: TypesLabelValue[]
}
export enum LabelFilterType {
LABEL = 'label',
VALUE = 'value'
}
export interface LabelFilterObj {
labelId: number
valueId?: number
type: LabelFilterType
labelObj: TypesLabel
valueObj?: TypesLabelValue
}
export enum LabelsPageScope {
ACCOUNT = 'acc',
ORG = 'org',
PROJECT = 'project',
SPACE = 'space',
REPOSITORY = 'repo'
}
export interface LabelListingProps {
currentPageScope: LabelsPageScope
repoMetadata?: RepoRepositoryOutput
space?: string
activeTab?: string
}
export const getScopeData = (space: string, scope: number, standalone: boolean) => {
const accountIdentifier = space?.split('/')[0]
const orgIdentifier = space?.split('/')[1]
const projectIdentifier = space?.split('/')[2]
if (standalone) {
return { scopeRef: space, scopeIcon: 'nav-project' as IconName, scopeId: space }
}
switch (scope) {
case 1:
return { scopeRef: accountIdentifier as string, scopeIcon: 'Account' as IconName, scopeId: accountIdentifier }
case 2:
return {
scopeRef: `${accountIdentifier}/${orgIdentifier}` as string,
scopeIcon: 'nav-organization' as IconName,
scopeId: orgIdentifier
}
case 3:
return {
scopeRef: `${accountIdentifier}/${orgIdentifier}/${projectIdentifier}` as string,
scopeIcon: 'nav-project' as IconName,
scopeId: projectIdentifier
}
default:
return { scopeRef: space, scopeIcon: 'nav-project' as IconName, scopeId: scope }
}
}