mirror of
https://github.com/harness/drone.git
synced 2025-05-03 07:09:48 +08:00
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:
parent
d2dcc9213b
commit
2d14111677
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
},
|
||||
|
@ -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 : ''
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
98
web/src/components/Label/Label.module.scss
Normal file
98
web/src/components/Label/Label.module.scss
Normal 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%;
|
||||
}
|
29
web/src/components/Label/Label.module.scss.d.ts
vendored
Normal file
29
web/src/components/Label/Label.module.scss.d.ts
vendored
Normal 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
|
336
web/src/components/Label/Label.tsx
Normal file
336
web/src/components/Label/Label.tsx
Normal 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>
|
||||
)
|
||||
}
|
154
web/src/components/Label/LabelFilter/LabelFilter.module.scss
Normal file
154
web/src/components/Label/LabelFilter/LabelFilter.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
34
web/src/components/Label/LabelFilter/LabelFilter.module.scss.d.ts
vendored
Normal file
34
web/src/components/Label/LabelFilter/LabelFilter.module.scss.d.ts
vendored
Normal 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
|
571
web/src/components/Label/LabelFilter/LabelFilter.tsx
Normal file
571
web/src/components/Label/LabelFilter/LabelFilter.tsx
Normal 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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
136
web/src/components/Label/LabelSelector/LabelSelector.module.scss
Normal file
136
web/src/components/Label/LabelSelector/LabelSelector.module.scss
Normal 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;
|
||||
}
|
36
web/src/components/Label/LabelSelector/LabelSelector.module.scss.d.ts
vendored
Normal file
36
web/src/components/Label/LabelSelector/LabelSelector.module.scss.d.ts
vendored
Normal 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
|
555
web/src/components/Label/LabelSelector/LabelSelector.tsx
Normal file
555
web/src/components/Label/LabelSelector/LabelSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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
|
||||
|
32
web/src/hooks/useGetCurrentPageScope.tsx
Normal file
32
web/src/hooks/useGetCurrentPageScope.tsx
Normal 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
|
||||
}
|
@ -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 doesn’t 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'
|
||||
|
@ -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
|
||||
})}
|
||||
|
102
web/src/pages/Labels/LabelModal/LabelModal.module.scss
Normal file
102
web/src/pages/Labels/LabelModal/LabelModal.module.scss
Normal 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;
|
||||
}
|
25
web/src/pages/Labels/LabelModal/LabelModal.module.scss.d.ts
vendored
Normal file
25
web/src/pages/Labels/LabelModal/LabelModal.module.scss.d.ts
vendored
Normal 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
|
557
web/src/pages/Labels/LabelModal/LabelModal.tsx
Normal file
557
web/src/pages/Labels/LabelModal/LabelModal.tsx
Normal 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
|
64
web/src/pages/Labels/LabelsHeader/LabelsHeader.module.scss
Normal file
64
web/src/pages/Labels/LabelsHeader/LabelsHeader.module.scss
Normal 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;
|
||||
}
|
28
web/src/pages/Labels/LabelsHeader/LabelsHeader.module.scss.d.ts
vendored
Normal file
28
web/src/pages/Labels/LabelsHeader/LabelsHeader.module.scss.d.ts
vendored
Normal 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
|
107
web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx
Normal file
107
web/src/pages/Labels/LabelsHeader/LabelsHeader.tsx
Normal 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
|
||||
}
|
98
web/src/pages/Labels/LabelsListing.module.scss
Normal file
98
web/src/pages/Labels/LabelsListing.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
31
web/src/pages/Labels/LabelsListing.module.scss.d.ts
vendored
Normal file
31
web/src/pages/Labels/LabelsListing.module.scss.d.ts
vendored
Normal 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
|
355
web/src/pages/Labels/LabelsListing.tsx
Normal file
355
web/src/pages/Labels/LabelsListing.tsx
Normal 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
|
@ -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);
|
||||
}
|
19
web/src/pages/ManageSpace/ManageLabels/ManageLabels.module.scss.d.ts
vendored
Normal file
19
web/src/pages/ManageSpace/ManageLabels/ManageLabels.module.scss.d.ts
vendored
Normal 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
|
44
web/src/pages/ManageSpace/ManageLabels/ManageLabels.tsx
Normal file
44
web/src/pages/ManageSpace/ManageLabels/ManageLabels.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -66,3 +66,10 @@
|
||||
.iconPadding {
|
||||
padding-left: 0.6rem !important;
|
||||
}
|
||||
|
||||
.labelsLayout {
|
||||
width: auto;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 7px;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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={() =>
|
||||
|
@ -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()}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)}>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
46
web/src/pages/SpaceSettings/GeneralSettings/GeneralSpaceSettings.module.scss.d.ts
vendored
Normal file
46
web/src/pages/SpaceSettings/GeneralSettings/GeneralSpaceSettings.module.scss.d.ts
vendored
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user