mirror of
https://github.com/harness/drone.git
synced 2025-05-03 20:39: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/harness/gitness/types/enum"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gotidy/ptr"
|
||||||
"github.com/guregu/null"
|
"github.com/guregu/null"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -56,9 +57,11 @@ type pullReqAssignmentInfo struct {
|
|||||||
LabelID int64 `db:"label_id"`
|
LabelID int64 `db:"label_id"`
|
||||||
LabelKey string `db:"label_key"`
|
LabelKey string `db:"label_key"`
|
||||||
LabelColor enum.LabelColor `db:"label_color"`
|
LabelColor enum.LabelColor `db:"label_color"`
|
||||||
|
LabelScope int64 `db:"label_scope"`
|
||||||
ValueCount int64 `db:"label_value_count"`
|
ValueCount int64 `db:"label_value_count"`
|
||||||
|
ValueID null.Int `db:"label_value_id"`
|
||||||
Value null.String `db:"label_value_value"`
|
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 (
|
const (
|
||||||
@ -193,7 +196,9 @@ func (s *pullReqLabelStore) ListAssignedByPullreqIDs(
|
|||||||
,label_id
|
,label_id
|
||||||
,label_key
|
,label_key
|
||||||
,label_color
|
,label_color
|
||||||
|
,label_scope
|
||||||
,label_value_count
|
,label_value_count
|
||||||
|
,label_value_id
|
||||||
,label_value_value
|
,label_value_value
|
||||||
,label_value_color
|
,label_value_color
|
||||||
`).
|
`).
|
||||||
@ -261,14 +266,20 @@ func mapPullReqLabel(lbl *pullReqLabel) *types.PullReqLabel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapPullReqAssignmentInfo(lbl *pullReqAssignmentInfo) *types.LabelPullReqAssignmentInfo {
|
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{
|
return &types.LabelPullReqAssignmentInfo{
|
||||||
PullReqID: lbl.PullReqID,
|
PullReqID: lbl.PullReqID,
|
||||||
LabelID: lbl.LabelID,
|
LabelID: lbl.LabelID,
|
||||||
LabelKey: lbl.LabelKey,
|
LabelKey: lbl.LabelKey,
|
||||||
LabelColor: lbl.LabelColor,
|
LabelColor: lbl.LabelColor,
|
||||||
|
LabelScope: lbl.LabelScope,
|
||||||
ValueCount: lbl.ValueCount,
|
ValueCount: lbl.ValueCount,
|
||||||
|
ValueID: lbl.ValueID.Ptr(),
|
||||||
Value: lbl.Value.Ptr(),
|
Value: lbl.Value.Ptr(),
|
||||||
ValueColor: lbl.ValueColor.Ptr(),
|
ValueColor: valueColor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,13 +95,15 @@ type LabelAssignment struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LabelPullReqAssignmentInfo struct {
|
type LabelPullReqAssignmentInfo struct {
|
||||||
PullReqID int64 `json:"-"`
|
PullReqID int64 `json:"-"`
|
||||||
LabelID int64 `json:"id"`
|
LabelID int64 `json:"id"`
|
||||||
LabelKey string `json:"key"`
|
LabelKey string `json:"key"`
|
||||||
LabelColor enum.LabelColor `json:"color,omitempty"`
|
LabelColor enum.LabelColor `json:"color,omitempty"`
|
||||||
ValueCount int64 `json:"value_count"`
|
LabelScope int64 `json:"scope"`
|
||||||
Value *string `json:"value,omitempty"`
|
ValueCount int64 `json:"value_count"`
|
||||||
ValueColor *string `json:"value_color,omitempty"`
|
ValueID *int64 `json:"value_id,omitempty"`
|
||||||
|
Value *string `json:"value,omitempty"`
|
||||||
|
ValueColor *enum.LabelColor `json:"value_color,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScopeData struct {
|
type ScopeData struct {
|
||||||
|
@ -52,6 +52,7 @@ module.exports = {
|
|||||||
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
|
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
|
||||||
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
|
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
|
||||||
'./Search': './src/pages/Search/CodeSearchPage.tsx',
|
'./Search': './src/pages/Search/CodeSearchPage.tsx',
|
||||||
|
'./Labels': './src/pages/ManageSpace/ManageLabels/ManageLabels.tsx',
|
||||||
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
||||||
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ export interface CODERoutes extends CDERoutes {
|
|||||||
toCODEHome: () => string
|
toCODEHome: () => string
|
||||||
|
|
||||||
toCODESpaceAccessControl: (args: Required<Pick<CODEProps, 'space'>>) => 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
|
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||||
toCODEPipelineSettings: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
toCODEPipelineSettings: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||||
@ -103,6 +103,7 @@ export interface CODERoutes extends CDERoutes {
|
|||||||
args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
|
args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
|
||||||
) => string
|
) => string
|
||||||
toCODESpaceSearch: (args: Required<Pick<CODEProps, 'space'>>) => string
|
toCODESpaceSearch: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||||
|
toCODESpaceLabels: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||||
toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||||
@ -126,7 +127,8 @@ export const routes: CODERoutes = {
|
|||||||
toCODEHome: () => `/`,
|
toCODEHome: () => `/`,
|
||||||
|
|
||||||
toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`,
|
toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`,
|
||||||
toCODESpaceSettings: ({ space }) => `/settings/${space}`,
|
toCODESpaceSettings: ({ space, settingSection }) =>
|
||||||
|
`/settings/${space}/project${settingSection ? '/' + settingSection : ''}`,
|
||||||
toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`,
|
toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`,
|
||||||
toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`,
|
toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`,
|
||||||
toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`,
|
toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`,
|
||||||
@ -157,6 +159,7 @@ export const routes: CODERoutes = {
|
|||||||
toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`,
|
toCODECompare: ({ repoPath, diffRefs }) => `/${repoPath}/pulls/compare/${diffRefs}`,
|
||||||
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
||||||
toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
|
toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
|
||||||
|
toCODESpaceLabels: ({ space }) => `/${space}/labels`,
|
||||||
toCODESettings: ({ repoPath, settingSection, ruleId, settingSectionMode }) =>
|
toCODESettings: ({ repoPath, settingSection, ruleId, settingSectionMode }) =>
|
||||||
`/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${
|
`/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${
|
||||||
settingSectionMode ? '/' + settingSectionMode : ''
|
settingSectionMode ? '/' + settingSectionMode : ''
|
||||||
|
@ -54,6 +54,7 @@ import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
|
|||||||
import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails'
|
import GitspaceDetails from 'cde-gitness/pages/GitspaceDetails/GitspaceDetails'
|
||||||
import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing'
|
import GitspaceListing from 'cde-gitness/pages/GitspaceListing/GitspaceListing'
|
||||||
import GitspaceCreate from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate'
|
import GitspaceCreate from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate'
|
||||||
|
import ManageLabels from 'pages/ManageSpace/ManageLabels/ManageLabels'
|
||||||
|
|
||||||
export const RouteDestinations: React.FC = React.memo(function RouteDestinations() {
|
export const RouteDestinations: React.FC = React.memo(function RouteDestinations() {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
@ -81,7 +82,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
</Route>
|
</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')}>
|
<LayoutWithSideNav title={getString('pageTitle.spaceSettings')}>
|
||||||
<SpaceSettings />
|
<SpaceSettings />
|
||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
@ -375,6 +381,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path={routes.toCODESpaceLabels({ space: pathProps.space })} exact>
|
||||||
|
<LayoutWithSideNav title={getString('labels.labels')}>
|
||||||
|
<ManageLabels />
|
||||||
|
</LayoutWithSideNav>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={[routes.toCODESpaceSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
|
path={[routes.toCODESpaceSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
|
||||||
exact>
|
exact>
|
||||||
|
@ -34,7 +34,14 @@ export enum CommentType {
|
|||||||
MERGE = 'merge',
|
MERGE = 'merge',
|
||||||
BRANCH_UPDATE = 'branch-update',
|
BRANCH_UPDATE = 'branch-update',
|
||||||
BRANCH_DELETE = 'branch-delete',
|
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
|
onButtonClick?: () => void
|
||||||
permissionProp?: { disabled: boolean; tooltip: JSX.Element | string } | undefined
|
permissionProp?: { disabled: boolean; tooltip: JSX.Element | string } | undefined
|
||||||
standalone?: boolean
|
standalone?: boolean
|
||||||
|
forFilter?: boolean
|
||||||
|
emptyFilterMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoResultCard: React.FC<NoResultCardProps> = ({
|
export const NoResultCard: React.FC<NoResultCardProps> = ({
|
||||||
showWhen = () => true,
|
showWhen = () => true,
|
||||||
forSearch,
|
forSearch,
|
||||||
|
forFilter = false,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
emptySearchMessage,
|
emptySearchMessage,
|
||||||
|
emptyFilterMessage,
|
||||||
buttonText = '',
|
buttonText = '',
|
||||||
buttonIcon = CodeIcon.Add,
|
buttonIcon = CodeIcon.Add,
|
||||||
onButtonClick = noop,
|
onButtonClick = noop,
|
||||||
@ -57,12 +61,16 @@ export const NoResultCard: React.FC<NoResultCardProps> = ({
|
|||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<NoDataCard
|
<NoDataCard
|
||||||
image={Images.EmptyState}
|
image={Images.EmptyState}
|
||||||
messageTitle={forSearch ? title || getString('noResultTitle') : undefined}
|
messageTitle={forSearch || forFilter ? title || getString('noResultTitle') : undefined}
|
||||||
message={
|
message={
|
||||||
forSearch ? emptySearchMessage || getString('noResultMessage') : message || getString('noResultMessage')
|
forSearch
|
||||||
|
? emptySearchMessage || getString('noResultMessage')
|
||||||
|
: forFilter
|
||||||
|
? emptyFilterMessage || getString('noFilterResultMessage')
|
||||||
|
: message || getString('noResultMessage')
|
||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
forSearch ? undefined : (
|
forSearch || forFilter ? undefined : (
|
||||||
<Button
|
<Button
|
||||||
variation={ButtonVariation.PRIMARY}
|
variation={ButtonVariation.PRIMARY}
|
||||||
text={buttonText}
|
text={buttonText}
|
||||||
|
@ -616,6 +616,60 @@ export interface StringsMap {
|
|||||||
'keywordSearch.sampleQueries.searchForPattern': string
|
'keywordSearch.sampleQueries.searchForPattern': string
|
||||||
keywordSearchPlaceholder: string
|
keywordSearchPlaceholder: string
|
||||||
killed: 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
|
language: string
|
||||||
leaveAComment: string
|
leaveAComment: string
|
||||||
license: string
|
license: string
|
||||||
@ -627,6 +681,7 @@ export interface StringsMap {
|
|||||||
makeRequired: string
|
makeRequired: string
|
||||||
manageApiToken: string
|
manageApiToken: string
|
||||||
manageCredText: string
|
manageCredText: string
|
||||||
|
manageRepository: string
|
||||||
markAsDraft: string
|
markAsDraft: string
|
||||||
matchPassword: string
|
matchPassword: string
|
||||||
mergeBranchTitle: string
|
mergeBranchTitle: string
|
||||||
@ -679,6 +734,7 @@ export interface StringsMap {
|
|||||||
noCommitsPR: string
|
noCommitsPR: string
|
||||||
noExpiration: string
|
noExpiration: string
|
||||||
noExpirationDate: string
|
noExpirationDate: string
|
||||||
|
noFilterResultMessage: string
|
||||||
noOptionalReviewers: string
|
noOptionalReviewers: string
|
||||||
noRequiredReviewers: string
|
noRequiredReviewers: string
|
||||||
noResultMessage: string
|
noResultMessage: string
|
||||||
@ -899,6 +955,7 @@ export interface StringsMap {
|
|||||||
public: string
|
public: string
|
||||||
pullMustBeMadeFromBranches: string
|
pullMustBeMadeFromBranches: string
|
||||||
pullRequestEmpty: string
|
pullRequestEmpty: string
|
||||||
|
pullRequestNotFoundforFilter: string
|
||||||
pullRequestalreadyExists: string
|
pullRequestalreadyExists: string
|
||||||
pullRequests: string
|
pullRequests: string
|
||||||
quote: 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
|
commitChanges: Commit changes
|
||||||
pullRequests: Pull Requests
|
pullRequests: Pull Requests
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
manageRepository: Manage Repository
|
||||||
newFile: New File
|
newFile: New File
|
||||||
editFile: Edit File
|
editFile: Edit File
|
||||||
prev: Prev
|
prev: Prev
|
||||||
@ -90,6 +91,7 @@ cloneHTTPS: Git clone URL
|
|||||||
nameYourFile: Name your file...
|
nameYourFile: Name your file...
|
||||||
noResultTitle: Sorry, no result found
|
noResultTitle: Sorry, no result found
|
||||||
noResultMessage: What you searched was unfortunately not found or doesn’t exist
|
noResultMessage: What you searched was unfortunately not found or doesn’t exist
|
||||||
|
noFilterResultMessage: No results were found based on the applied filters
|
||||||
pageTitle:
|
pageTitle:
|
||||||
signin: Sign In
|
signin: Sign In
|
||||||
register: Register a new account
|
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.'
|
fileDeleted: '__path__ successfully deleted.'
|
||||||
newPullRequest: New Pull Request
|
newPullRequest: New Pull Request
|
||||||
pullRequestEmpty: There are no pull requests in your repo. Click the button below to create a 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
|
comparingChanges: Comparing Changes
|
||||||
selectToViewMore: Select branch to view more here.
|
selectToViewMore: Select branch to view more here.
|
||||||
createPullRequest: Create pull request
|
createPullRequest: Create pull request
|
||||||
@ -1293,3 +1296,58 @@ regex:
|
|||||||
enabled: enabled
|
enabled: enabled
|
||||||
disabled: disabled
|
disabled: disabled
|
||||||
string: RegEx
|
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
|
<NavMenuItem
|
||||||
data-code-repo-section="settings"
|
data-code-repo-section="settings"
|
||||||
isSubLink
|
isSubLink
|
||||||
label={getString('settings')}
|
label={getString('manageRepository')}
|
||||||
to={routes.toCODESettings({
|
to={routes.toCODESettings({
|
||||||
repoPath
|
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 { useLocation } from 'react-router-dom'
|
||||||
import { useGet, useMutate } from 'restful-react'
|
import { useGet, useMutate } from 'restful-react'
|
||||||
import { get, orderBy } from 'lodash-es'
|
import { get, orderBy } from 'lodash-es'
|
||||||
|
import { Render } from 'react-jsx-match'
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
@ -38,7 +39,8 @@ import type {
|
|||||||
TypesPullReqStats,
|
TypesPullReqStats,
|
||||||
TypesCodeOwnerEvaluation,
|
TypesCodeOwnerEvaluation,
|
||||||
TypesPullReqReviewer,
|
TypesPullReqReviewer,
|
||||||
TypesListCommitResponse
|
TypesListCommitResponse,
|
||||||
|
TypesScopesLabels
|
||||||
} from 'services/code'
|
} from 'services/code'
|
||||||
import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox'
|
import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox'
|
||||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||||
@ -50,7 +52,7 @@ import {
|
|||||||
filenameToLanguage,
|
filenameToLanguage,
|
||||||
PRCommentFilterType
|
PRCommentFilterType
|
||||||
} from 'utils/Utils'
|
} 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 { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
||||||
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
import { ThreadSection } from 'components/ThreadSection/ThreadSection'
|
||||||
import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect'
|
import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect'
|
||||||
@ -59,6 +61,7 @@ import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondaryS
|
|||||||
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
||||||
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
||||||
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
|
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
|
||||||
|
import { getConfig } from 'services/config'
|
||||||
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
|
||||||
import { DescriptionBox } from './DescriptionBox'
|
import { DescriptionBox } from './DescriptionBox'
|
||||||
import PullRequestSideBar from './PullRequestSideBar/PullRequestSideBar'
|
import PullRequestSideBar from './PullRequestSideBar/PullRequestSideBar'
|
||||||
@ -93,7 +96,8 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
pullReqCommits
|
pullReqCommits
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { currentUser, routes } = useAppContext()
|
const { currentUser, routes, hooks } = useAppContext()
|
||||||
|
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const activities = usePullReqActivities()
|
const activities = usePullReqActivities()
|
||||||
const {
|
const {
|
||||||
@ -105,6 +109,12 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
debounce: 500
|
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>({
|
const { data: codeOwners, refetch: refetchCodeOwners } = useGet<TypesCodeOwnerEvaluation>({
|
||||||
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/codeowners`,
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/codeowners`,
|
||||||
debounce: 500
|
debounce: 500
|
||||||
@ -256,21 +266,24 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
() =>
|
() =>
|
||||||
activityBlocks?.map((commentItems, index) => {
|
activityBlocks?.map((commentItems, index) => {
|
||||||
const threadId = commentItems[0].payload?.id
|
const threadId = commentItems[0].payload?.id
|
||||||
|
const renderLabelActivities =
|
||||||
|
commentItems[0].payload?.type !== CommentType.LABEL_MODIFY || isLabelEnabled || standalone
|
||||||
if (isSystemComment(commentItems)) {
|
if (isSystemComment(commentItems)) {
|
||||||
return (
|
return (
|
||||||
<ThreadSection
|
<Render key={`thread-${threadId}`} when={renderLabelActivities}>
|
||||||
key={`thread-${threadId}`}
|
<ThreadSection
|
||||||
onlyTitle
|
key={`thread-${threadId}`}
|
||||||
lastItem={activityBlocks.length - 1 === index}
|
onlyTitle
|
||||||
title={
|
lastItem={activityBlocks.length - 1 === index}
|
||||||
<SystemComment
|
title={
|
||||||
key={`system-${threadId}`}
|
<SystemComment
|
||||||
pullReqMetadata={pullReqMetadata}
|
key={`system-${threadId}`}
|
||||||
commentItems={commentItems}
|
pullReqMetadata={pullReqMetadata}
|
||||||
repoMetadataPath={repoMetadata.path}
|
commentItems={commentItems}
|
||||||
/>
|
repoMetadataPath={repoMetadata.path}
|
||||||
}></ThreadSection>
|
/>
|
||||||
|
}></ThreadSection>
|
||||||
|
</Render>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,6 +506,8 @@ export const Conversation: React.FC<ConversationProps> = ({
|
|||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
pullRequestMetadata={pullReqMetadata}
|
pullRequestMetadata={pullReqMetadata}
|
||||||
refetchReviewers={refetchReviewers}
|
refetchReviewers={refetchReviewers}
|
||||||
|
labels={labels}
|
||||||
|
refetchLabels={refetchLabels}
|
||||||
/>
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -66,3 +66,10 @@
|
|||||||
.iconPadding {
|
.iconPadding {
|
||||||
padding-left: 0.6rem !important;
|
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
|
// This is an auto-generated file
|
||||||
export declare const alignLayout: string
|
export declare const alignLayout: string
|
||||||
export declare const iconPadding: string
|
export declare const iconPadding: string
|
||||||
|
export declare const labelsLayout: string
|
||||||
export declare const redIcon: string
|
export declare const redIcon: string
|
||||||
export declare const reviewerAvatar: string
|
export declare const reviewerAvatar: string
|
||||||
export declare const reviewerName: string
|
export declare const reviewerName: string
|
||||||
|
@ -14,38 +14,44 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { PopoverInteractionKind } from '@blueprintjs/core'
|
import { PopoverInteractionKind } from '@blueprintjs/core'
|
||||||
import { useMutate } from 'restful-react'
|
import { useGet, useMutate } from 'restful-react'
|
||||||
import { omit } from 'lodash-es'
|
import { omit } from 'lodash-es'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
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 { Icon, IconName } from '@harnessio/icons'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
|
import { Render } from 'react-jsx-match'
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import type { TypesPullReq, RepoRepositoryOutput, EnumPullReqReviewDecision } from 'services/code'
|
import type { TypesPullReq, RepoRepositoryOutput, EnumPullReqReviewDecision, TypesScopesLabels } from 'services/code'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { ColorName, getErrorMessage } from 'utils/Utils'
|
||||||
import { ReviewerSelect } from 'components/ReviewerSelect/ReviewerSelect'
|
import { ReviewerSelect } from 'components/ReviewerSelect/ReviewerSelect'
|
||||||
import { PullReqReviewDecision, processReviewDecision } from 'pages/PullRequest/PullRequestUtils'
|
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 ignoreFailed from '../../../../icons/ignoreFailed.svg?url'
|
||||||
import css from './PullRequestSideBar.module.scss'
|
import css from './PullRequestSideBar.module.scss'
|
||||||
|
|
||||||
interface PullRequestSideBarProps {
|
interface PullRequestSideBarProps {
|
||||||
reviewers?: Unknown
|
reviewers?: Unknown
|
||||||
|
labels: TypesScopesLabels | null
|
||||||
repoMetadata: RepoRepositoryOutput
|
repoMetadata: RepoRepositoryOutput
|
||||||
pullRequestMetadata: TypesPullReq
|
pullRequestMetadata: TypesPullReq
|
||||||
refetchReviewers: () => void
|
refetchReviewers: () => void
|
||||||
|
refetchLabels: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PullRequestSideBar = (props: PullRequestSideBarProps) => {
|
const PullRequestSideBar = (props: PullRequestSideBarProps) => {
|
||||||
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers } = props
|
const { standalone, hooks } = useAppContext()
|
||||||
// const [searchTerm, setSearchTerm] = useState('')
|
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
|
||||||
// const [page] = usePageIndex(1)
|
const [labelQuery, setLabelQuery] = useState<string>('')
|
||||||
|
const { reviewers, repoMetadata, pullRequestMetadata, refetchReviewers, labels, refetchLabels } = props
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
// const tagArr = []
|
const { showError, showSuccess } = useToaster()
|
||||||
const { showError } = useToaster()
|
|
||||||
|
|
||||||
const generateReviewDecisionInfo = (
|
const generateReviewDecisionInfo = (
|
||||||
reviewDecision: EnumPullReqReviewDecision | PullReqReviewDecision.outdated
|
reviewDecision: EnumPullReqReviewDecision | PullReqReviewDecision.outdated
|
||||||
): {
|
): {
|
||||||
@ -159,6 +165,27 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
|
|||||||
verb: 'DELETE',
|
verb: 'DELETE',
|
||||||
path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}`
|
path: ({ id }) => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviewers/${id}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { mutate: removeLabel } = useMutate({
|
||||||
|
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 [isOptionsOpen, setOptionsOpen] = React.useState(false)
|
||||||
// const [val, setVal] = useState<SelectOption>()
|
// 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
|
//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>
|
</Text>
|
||||||
)} */}
|
)} */}
|
||||||
</Layout.Vertical>
|
</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>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
@ -19,16 +19,18 @@ import { Avatar, Container, Layout, StringSubstitute, Text } from '@harnessio/ui
|
|||||||
import { Icon, IconName } from '@harnessio/icons'
|
import { Icon, IconName } from '@harnessio/icons'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
import { defaultTo } from 'lodash-es'
|
import { defaultTo } from 'lodash-es'
|
||||||
|
import { Case, Match } from 'react-jsx-match'
|
||||||
import { CodeIcon, GitInfoProps, MergeStrategy } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps, MergeStrategy } from 'utils/GitUtils'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import type { TypesPullReqActivity } from 'services/code'
|
import type { TypesPullReqActivity } from 'services/code'
|
||||||
import type { CommentItem } from 'components/CommentBox/CommentBox'
|
import type { CommentItem } from 'components/CommentBox/CommentBox'
|
||||||
import { PullRequestSection } from 'utils/Utils'
|
import { PullRequestSection } from 'utils/Utils'
|
||||||
import { CommentType } from 'components/DiffViewer/DiffViewerUtils'
|
import { CommentType, LabelActivity } from 'components/DiffViewer/DiffViewerUtils'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { CommitActions } from 'components/CommitActions/CommitActions'
|
import { CommitActions } from 'components/CommitActions/CommitActions'
|
||||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
|
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
|
||||||
|
import { Label } from 'components/Label/Label'
|
||||||
import css from './Conversation.module.scss'
|
import css from './Conversation.module.scss'
|
||||||
|
|
||||||
interface SystemCommentProps extends Pick<GitInfoProps, 'pullReqMetadata'> {
|
interface SystemCommentProps extends Pick<GitInfoProps, 'pullReqMetadata'> {
|
||||||
@ -41,7 +43,7 @@ interface MergePayload {
|
|||||||
merge_method: string
|
merge_method: string
|
||||||
rules_bypassed: boolean
|
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 }) => {
|
export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, commentItems, repoMetadataPath }) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const payload = commentItems[0].payload
|
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: {
|
default: {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Unable to render system type activity', commentItems)
|
console.warn('Unable to render system type activity', commentItems)
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
.table {
|
.table {
|
||||||
.row {
|
.row {
|
||||||
height: 80px;
|
height: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -37,6 +37,16 @@
|
|||||||
.convo {
|
.convo {
|
||||||
white-space: nowrap;
|
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 {
|
.rowLink:hover {
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
export declare const convo: string
|
export declare const convo: string
|
||||||
export declare const convoIcon: string
|
export declare const convoIcon: string
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
|
export declare const prLabels: string
|
||||||
|
export declare const prwrap: string
|
||||||
export declare const row: string
|
export declare const row: string
|
||||||
export declare const rowLink: string
|
export declare const rowLink: string
|
||||||
export declare const state: string
|
export declare const state: string
|
||||||
|
@ -15,25 +15,44 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
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 { Icon } from '@harnessio/icons'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
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 { useGet } from 'restful-react'
|
||||||
import type { CellProps, Column } from 'react-table'
|
import type { CellProps, Column } from 'react-table'
|
||||||
import { Case, Match, Render, Truthy } from 'react-jsx-match'
|
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 { makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
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 { usePageIndex } from 'hooks/usePageIndex'
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
|
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
|
||||||
import { useQueryParams } from 'hooks/useQueryParams'
|
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 { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
||||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||||
@ -42,6 +61,7 @@ import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequ
|
|||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
import useSpaceSSE from 'hooks/useSpaceSSE'
|
import useSpaceSSE from 'hooks/useSpaceSSE'
|
||||||
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
|
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
|
||||||
|
import { Label } from 'components/Label/Label'
|
||||||
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
|
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
|
||||||
import css from './PullRequests.module.scss'
|
import css from './PullRequests.module.scss'
|
||||||
|
|
||||||
@ -50,11 +70,13 @@ const SSE_EVENTS = ['pullreq_updated']
|
|||||||
export default function PullRequests() {
|
export default function PullRequests() {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const history = useHistory()
|
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 [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||||
const browserParams = useQueryParams<PageBrowserProps>()
|
const browserParams = useQueryParams<PageBrowserProps>()
|
||||||
const [filter, setFilter] = useState(browserParams?.state || (PullRequestFilterOption.OPEN as string))
|
const [filter, setFilter] = useState(browserParams?.state || (PullRequestFilterOption.OPEN as string))
|
||||||
const [authorFilter, setAuthorFilter] = useState<string>()
|
const [authorFilter, setAuthorFilter] = useState<string>()
|
||||||
|
const [labelFilter, setLabelFilter] = useState<LabelFilterObj[]>([])
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
|
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
|
||||||
const pageInit = browserParams.page ? parseInt(browserParams.page) : 1
|
const pageInit = browserParams.page ? parseInt(browserParams.page) : 1
|
||||||
@ -89,7 +111,21 @@ export default function PullRequests() {
|
|||||||
order: 'desc',
|
order: 'desc',
|
||||||
query: searchTerm,
|
query: searchTerm,
|
||||||
state: browserParams.state ? browserParams.state : filter == PullRequestFilterOption.ALL ? '' : filter,
|
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,
|
debounce: 500,
|
||||||
lazy: !repoMetadata
|
lazy: !repoMetadata
|
||||||
@ -112,8 +148,6 @@ export default function PullRequests() {
|
|||||||
onEvent: eventHandler
|
onEvent: eventHandler
|
||||||
})
|
})
|
||||||
|
|
||||||
const { standalone } = useAppContext()
|
|
||||||
const { hooks } = useAppContext()
|
|
||||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||||
{
|
{
|
||||||
resource: {
|
resource: {
|
||||||
@ -125,6 +159,76 @@ export default function PullRequests() {
|
|||||||
[space]
|
[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(
|
const columns: Column<TypesPullReq>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -132,32 +236,63 @@ export default function PullRequests() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
Cell: ({ row }: CellProps<TypesPullReq>) => {
|
Cell: ({ row }: CellProps<TypesPullReq>) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Container
|
||||||
className={css.rowLink}
|
className={css.rowLink}
|
||||||
to={routes.toCODEPullRequest({
|
onClick={() =>
|
||||||
repoPath: repoMetadata?.path as string,
|
history.push(
|
||||||
pullRequestId: String(row.original.number)
|
routes.toCODEPullRequest({
|
||||||
})}>
|
repoPath: repoMetadata?.path as string,
|
||||||
|
pullRequestId: String(row.original.number)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}>
|
||||||
<Layout.Horizontal className={css.titleRow} spacing="medium">
|
<Layout.Horizontal className={css.titleRow} spacing="medium">
|
||||||
<PullRequestStateLabel iconSize={22} data={row.original} iconOnly />
|
<PullRequestStateLabel iconSize={22} data={row.original} iconOnly />
|
||||||
<Container padding={{ left: 'small' }}>
|
<Container padding={{ left: 'small' }}>
|
||||||
<Layout.Vertical spacing="xsmall">
|
<Layout.Vertical spacing="small">
|
||||||
<Container>
|
<Container>
|
||||||
<Layout.Horizontal>
|
<Layout.Horizontal flex={{ alignItems: 'center' }} className={css.prLabels}>
|
||||||
<Text color={Color.GREY_800} className={css.title} lineClamp={1}>
|
<Layout.Horizontal spacing={'xsmall'}>
|
||||||
{row.original.title}
|
<Text color={Color.GREY_800} className={css.title} lineClamp={1}>
|
||||||
</Text>
|
{row.original.title}
|
||||||
<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}
|
|
||||||
</Text>
|
</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>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
<Container>
|
<Container>
|
||||||
@ -223,7 +358,7 @@ export default function PullRequests() {
|
|||||||
{/* TODO: Pass proper state when check api is fully implemented */}
|
{/* TODO: Pass proper state when check api is fully implemented */}
|
||||||
{/* <ExecutionStatusLabel data={{ state: 'success' }} /> */}
|
{/* <ExecutionStatusLabel data={{ state: 'success' }} /> */}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Link>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,12 +391,72 @@ export default function PullRequests() {
|
|||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
activePullRequestAuthorFilterOption={authorFilter}
|
activePullRequestAuthorFilterOption={authorFilter}
|
||||||
|
activePullRequestLabelFilterOption={labelFilter}
|
||||||
onPullRequestAuthorFilterChanged={_authorFilter => {
|
onPullRequestAuthorFilterChanged={_authorFilter => {
|
||||||
setAuthorFilter(_authorFilter)
|
setAuthorFilter(_authorFilter)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
|
onPullRequestLabelFilterChanged={_labelFilter => {
|
||||||
|
setLabelFilter(_labelFilter)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Container padding="xlarge">
|
<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}>
|
<Match expr={data?.length}>
|
||||||
<Truthy>
|
<Truthy>
|
||||||
<>
|
<>
|
||||||
@ -279,6 +474,8 @@ export default function PullRequests() {
|
|||||||
<Case val={0}>
|
<Case val={0}>
|
||||||
<NoResultCard
|
<NoResultCard
|
||||||
forSearch={!!searchTerm}
|
forSearch={!!searchTerm}
|
||||||
|
forFilter={!isEmpty(labelFilter) || !isEmpty(authorFilter)}
|
||||||
|
emptyFilterMessage={getString('pullRequestNotFoundforFilter')}
|
||||||
message={getString('pullRequestEmpty')}
|
message={getString('pullRequestEmpty')}
|
||||||
buttonText={getString('newPullRequest')}
|
buttonText={getString('newPullRequest')}
|
||||||
onButtonClick={() =>
|
onButtonClick={() =>
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from '@harnessio/uicore'
|
} from '@harnessio/uicore'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
import { sortBy } from 'lodash-es'
|
import { sortBy } from 'lodash-es'
|
||||||
|
import { Render } from 'react-jsx-match'
|
||||||
import { getConfig, getUsingFetch } from 'services/config'
|
import { getConfig, getUsingFetch } from 'services/config'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
|
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 type { TypesPrincipalInfo, TypesUser } from 'services/code'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
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 { useQueryParams } from 'hooks/useQueryParams'
|
||||||
|
import { LabelFilter } from 'components/Label/LabelFilter/LabelFilter'
|
||||||
import css from './PullRequestsContentHeader.module.scss'
|
import css from './PullRequestsContentHeader.module.scss'
|
||||||
|
|
||||||
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
activePullRequestFilterOption?: string
|
activePullRequestFilterOption?: string
|
||||||
activePullRequestAuthorFilterOption?: string
|
activePullRequestAuthorFilterOption?: string
|
||||||
|
activePullRequestLabelFilterOption?: LabelFilterObj[]
|
||||||
onPullRequestFilterChanged: React.Dispatch<React.SetStateAction<string>>
|
onPullRequestFilterChanged: React.Dispatch<React.SetStateAction<string>>
|
||||||
onPullRequestAuthorFilterChanged: (authorFilter: string) => void
|
onPullRequestAuthorFilterChanged: (authorFilter: string) => void
|
||||||
|
onPullRequestLabelFilterChanged: (labelFilter: LabelFilterObj[]) => void
|
||||||
onSearchTermChanged: (searchTerm: string) => void
|
onSearchTermChanged: (searchTerm: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +56,11 @@ export function PullRequestsContentHeader({
|
|||||||
loading,
|
loading,
|
||||||
onPullRequestFilterChanged,
|
onPullRequestFilterChanged,
|
||||||
onPullRequestAuthorFilterChanged,
|
onPullRequestAuthorFilterChanged,
|
||||||
|
onPullRequestLabelFilterChanged,
|
||||||
onSearchTermChanged,
|
onSearchTermChanged,
|
||||||
activePullRequestFilterOption = PullRequestFilterOption.OPEN,
|
activePullRequestFilterOption = PullRequestFilterOption.OPEN,
|
||||||
activePullRequestAuthorFilterOption,
|
activePullRequestAuthorFilterOption,
|
||||||
|
activePullRequestLabelFilterOption,
|
||||||
repoMetadata
|
repoMetadata
|
||||||
}: PullRequestsContentHeaderProps) {
|
}: PullRequestsContentHeaderProps) {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
@ -62,11 +68,13 @@ export function PullRequestsContentHeader({
|
|||||||
const browserParams = useQueryParams<PageBrowserProps>()
|
const browserParams = useQueryParams<PageBrowserProps>()
|
||||||
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
|
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
|
||||||
const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption)
|
const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption)
|
||||||
|
const [labelFilterOption, setLabelFilterOption] = useState(activePullRequestLabelFilterOption)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [query, setQuery] = useState<string>('')
|
const [query, setQuery] = useState<string>('')
|
||||||
const [loadingAuthors, setLoadingAuthors] = useState<boolean>(false)
|
const [loadingAuthors, setLoadingAuthors] = useState<boolean>(false)
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const { hooks, currentUser, standalone, routingId, routes } = useAppContext()
|
const { hooks, currentUser, standalone, routingId, routes } = useAppContext()
|
||||||
|
const { CODE_PULLREQ_LABELS: isLabelEnabled } = hooks?.useFeatureFlags()
|
||||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||||
{
|
{
|
||||||
resource: {
|
resource: {
|
||||||
@ -78,6 +86,10 @@ export function PullRequestsContentHeader({
|
|||||||
[space]
|
[space]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLabelFilterOption(activePullRequestLabelFilterOption)
|
||||||
|
}, [activePullRequestLabelFilterOption])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilterOption(browserParams?.state as string)
|
setFilterOption(browserParams?.state as string)
|
||||||
}, [browserParams])
|
}, [browserParams])
|
||||||
@ -176,6 +188,16 @@ export function PullRequestsContentHeader({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FlexExpander />
|
<FlexExpander />
|
||||||
|
<Render when={isLabelEnabled || standalone}>
|
||||||
|
<LabelFilter
|
||||||
|
labelFilterOption={labelFilterOption}
|
||||||
|
setLabelFilterOption={setLabelFilterOption}
|
||||||
|
onPullRequestLabelFilterChanged={onPullRequestLabelFilterChanged}
|
||||||
|
bearerToken={bearerToken}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
spaceRef={space}
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
<DropDown
|
<DropDown
|
||||||
value={authorFilterOption}
|
value={authorFilterOption}
|
||||||
items={() => getAuthorsPromise()}
|
items={() => getAuthorsPromise()}
|
||||||
|
@ -48,7 +48,7 @@ import Private from '../../../icons/private.svg?url'
|
|||||||
import css from '../RepositorySettings.module.scss'
|
import css from '../RepositorySettings.module.scss'
|
||||||
|
|
||||||
interface GeneralSettingsProps {
|
interface GeneralSettingsProps {
|
||||||
repoMetadata: RepoRepositoryOutput | undefined
|
repoMetadata?: RepoRepositoryOutput
|
||||||
refetch: () => void
|
refetch: () => void
|
||||||
gitRef: string
|
gitRef: string
|
||||||
isRepositoryEmpty: boolean
|
isRepositoryEmpty: boolean
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
max-height: calc(var(--page-height) - 160px);
|
min-height: calc(var(--page-height) - 160px);
|
||||||
background-color: var(--primary-bg) !important;
|
background-color: var(--primary-bg) !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: var(--spacing-small);
|
margin: var(--spacing-small);
|
||||||
@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
.bp3-tab-panel {
|
.bp3-tab-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 500px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-tab {
|
.bp3-tab {
|
||||||
|
@ -24,20 +24,24 @@ import { useDisableCodeMainLinks } from 'hooks/useDisableCodeMainLinks'
|
|||||||
import { useGetResourceContent } from 'hooks/useGetResourceContent'
|
import { useGetResourceContent } from 'hooks/useGetResourceContent'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
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 { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
// import Webhooks from 'pages/Webhooks/Webhooks'
|
// import Webhooks from 'pages/Webhooks/Webhooks'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import BranchProtectionListing from 'components/BranchProtection/BranchProtectionListing'
|
import BranchProtectionListing from 'components/BranchProtection/BranchProtectionListing'
|
||||||
import { SettingsTab, normalizeGitRef } from 'utils/GitUtils'
|
import { SettingsTab, normalizeGitRef } from 'utils/GitUtils'
|
||||||
import SecurityScanSettings from 'pages/RepositorySettings/SecurityScanSettings/SecurityScanSettings'
|
import SecurityScanSettings from 'pages/RepositorySettings/SecurityScanSettings/SecurityScanSettings'
|
||||||
|
import LabelsListing from 'pages/Labels/LabelsListing'
|
||||||
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import GeneralSettingsContent from './GeneralSettingsContent/GeneralSettingsContent'
|
import GeneralSettingsContent from './GeneralSettingsContent/GeneralSettingsContent'
|
||||||
import css from './RepositorySettings.module.scss'
|
import css from './RepositorySettings.module.scss'
|
||||||
|
|
||||||
export default function RepositorySettings() {
|
export default function RepositorySettings() {
|
||||||
const { repoMetadata, error, loading, refetch, settingSection, gitRef, resourcePath } = useGetRepositoryMetadata()
|
const { repoMetadata, error, loading, refetch, settingSection, gitRef, resourcePath } = useGetRepositoryMetadata()
|
||||||
|
const space = useGetSpaceParam()
|
||||||
const history = useHistory()
|
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 [activeTab, setActiveTab] = React.useState<string>(settingSection || SettingsTab.general)
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { isRepositoryEmpty } = useGetResourceContent({
|
const { isRepositoryEmpty } = useGetResourceContent({
|
||||||
@ -71,7 +75,24 @@ export default function RepositorySettings() {
|
|||||||
id: SettingsTab.security,
|
id: SettingsTab.security,
|
||||||
title: getString('security'),
|
title: getString('security'),
|
||||||
panel: <SecurityScanSettings repoMetadata={repoMetadata} activeTab={activeTab} />
|
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,
|
// id: SettingsTab.webhooks,
|
||||||
// title: getString('webhooks'),
|
// title: getString('webhooks'),
|
||||||
@ -87,7 +108,7 @@ export default function RepositorySettings() {
|
|||||||
<RepositoryPageHeader
|
<RepositoryPageHeader
|
||||||
className={css.headerContainer}
|
className={css.headerContainer}
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
title={getString('settings')}
|
title={getString('manageRepository')}
|
||||||
dataTooltipId="repositorySettings"
|
dataTooltipId="repositorySettings"
|
||||||
/>
|
/>
|
||||||
<PageBody error={getErrorMessage(error)} retryOnError={voidFn(refetch)}>
|
<PageBody error={getErrorMessage(error)} retryOnError={voidFn(refetch)}>
|
||||||
|
@ -40,7 +40,7 @@ import type { RepoRepositoryOutput } from 'services/code'
|
|||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import type { ExportFormDataExtended } from 'utils/GitUtils'
|
import type { ExportFormDataExtended } from 'utils/GitUtils'
|
||||||
import Upgrade from '../../../icons/Upgrade.svg?url'
|
import Upgrade from '../../../icons/Upgrade.svg?url'
|
||||||
import css from '../SpaceSettings.module.scss'
|
import css from '../GeneralSettings/GeneralSpaceSettings.module.scss'
|
||||||
|
|
||||||
interface ExportFormProps {
|
interface ExportFormProps {
|
||||||
handleSubmit: (data: ExportFormDataExtended) => void
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mainCtn {
|
.main {
|
||||||
height: var(--page-height);
|
min-height: calc(var(--page-height) - 160px);
|
||||||
background-color: var(--primary-bg) !important;
|
background-color: var(--primary-bg) !important;
|
||||||
|
width: 100%;
|
||||||
|
margin: var(--spacing-small);
|
||||||
|
:global {
|
||||||
|
.bp3-tab {
|
||||||
|
width: fit-content !important;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
.roleBadge {
|
.bp3-tab-panel {
|
||||||
text-transform: capitalize;
|
width: 100%;
|
||||||
padding: var(--spacing-xsmall) 6px;
|
}
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--grey-200);
|
.bp3-tab {
|
||||||
background: var(--grey-50);
|
margin-top: 20px;
|
||||||
width: max-content;
|
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 {
|
.dividerContainer {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@ -35,23 +110,10 @@
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.dialogContainer {
|
||||||
width: 100px;
|
:global(.bp3-dialog-header) {
|
||||||
padding-right: var(--spacing-medium);
|
margin-bottom: var(--spacing-medium) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 93%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteBtn {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
min-width: auto !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.saveBtn {
|
.saveBtn {
|
||||||
@ -59,153 +121,87 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.textContainer {
|
.textContainer {
|
||||||
width: 80%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editContainer {
|
||||||
|
width: 60% !important;
|
||||||
|
textarea {
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
padding-top: var(--spacing-xsmall) !important;
|
padding-top: var(--spacing-xsmall) !important;
|
||||||
}
|
gap: 10px;
|
||||||
|
margin-left: auto !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 {
|
.textSize {
|
||||||
font-size: 13px !important;
|
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 */
|
/* eslint-disable */
|
||||||
// This is an auto-generated file
|
// This is an auto-generated file
|
||||||
export declare const button: string
|
export declare const btn: string
|
||||||
export declare const buttonContainer: string
|
export declare const buttonContainer: string
|
||||||
export declare const buttonText: string
|
|
||||||
export declare const checkbox: string
|
|
||||||
export declare const content: string
|
export declare const content: string
|
||||||
export declare const deleteBtn: string
|
export declare const contentContainer: string
|
||||||
export declare const deleteContainer: 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 dividerContainer: string
|
||||||
|
export declare const editContainer: string
|
||||||
export declare const generalContainer: string
|
export declare const generalContainer: string
|
||||||
export declare const harnessUpgradeWatermark: string
|
export declare const headerContainer: string
|
||||||
export declare const harnessWatermark: string
|
export declare const iconContainer: string
|
||||||
export declare const icon: string
|
|
||||||
export declare const importContainer: string
|
|
||||||
export declare const label: string
|
export declare const label: string
|
||||||
export declare const mainContainer: string
|
export declare const main: string
|
||||||
export declare const mainCtn: string
|
export declare const radioContainer: string
|
||||||
export declare const progressBar: string
|
|
||||||
export declare const repoInfo: string
|
|
||||||
export declare const roleBadge: string
|
|
||||||
export declare const saveBtn: 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 textContainer: string
|
||||||
export declare const textSize: string
|
export declare const textSize: string
|
||||||
export declare const upgradeButton: string
|
export declare const webhookHeader: string
|
||||||
export declare const upgradeContainer: string
|
export declare const webhooksContent: string
|
||||||
export declare const upgradeHeader: string
|
|
||||||
export declare const verticalContainer: string
|
|
||||||
export declare const yellowContainer: string
|
|
||||||
|
@ -14,507 +14,67 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React from 'react'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
Container,
|
|
||||||
Formik,
|
|
||||||
Layout,
|
|
||||||
Page,
|
|
||||||
ButtonVariation,
|
|
||||||
ButtonSize,
|
|
||||||
FlexExpander,
|
|
||||||
useToaster,
|
|
||||||
Heading,
|
|
||||||
TextInput,
|
|
||||||
stringSubstitute
|
|
||||||
} from '@harnessio/uicore'
|
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { noop } from 'lodash-es'
|
|
||||||
import { useMutate, useGet } from 'restful-react'
|
import { PageBody, Container, Tabs, Page } from '@harnessio/uicore'
|
||||||
import { Intent, Color, FontVariation } from '@harnessio/design-system'
|
|
||||||
import { useHistory } from 'react-router-dom'
|
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 { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
import { JobProgress, useGetSpace } from 'services/code'
|
|
||||||
import { useAppContext } from 'AppContext'
|
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { useAppContext } from 'AppContext'
|
||||||
import { ACCESS_MODES, permissionProps, voidFn } from 'utils/Utils'
|
import { SettingsTab, SpaceSettingsTab } from 'utils/GitUtils'
|
||||||
import type { ExportFormDataExtended } from 'utils/GitUtils'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import { useModalHook } from 'hooks/useModalHook'
|
import LabelsListing from 'pages/Labels/LabelsListing'
|
||||||
import useSpaceSSE from 'hooks/useSpaceSSE'
|
import { LabelsPageScope } from 'utils/Utils'
|
||||||
import Harness from '../../icons/Harness.svg?url'
|
import GeneralSpaceSettings from './GeneralSettings/GeneralSpaceSettings'
|
||||||
import Upgrade from '../../icons/Upgrade.svg?url'
|
|
||||||
import useDeleteSpaceModal from './DeleteSpaceModal/DeleteSpaceModal'
|
|
||||||
import ExportForm from './ExportForm/ExportForm'
|
|
||||||
import css from './SpaceSettings.module.scss'
|
import css from './SpaceSettings.module.scss'
|
||||||
|
|
||||||
export default function SpaceSettings() {
|
export default function SpaceSettings() {
|
||||||
const { space } = useGetRepositoryMetadata()
|
const { settingSection } = 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 history = useHistory()
|
||||||
const { routes, standalone, hooks } = useAppContext()
|
const { routes } = useAppContext()
|
||||||
//check upgrading for space
|
const space = useGetSpaceParam()
|
||||||
const [upgrading, setUpgrading] = useState(false)
|
const [activeTab, setActiveTab] = React.useState<string>(settingSection || SpaceSettingsTab.general)
|
||||||
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 { 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 = () => {
|
const tabListArray = [
|
||||||
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?.(
|
|
||||||
{
|
{
|
||||||
resource: {
|
id: SettingsTab.general,
|
||||||
resourceType: 'CODE_REPOSITORY'
|
title: 'General',
|
||||||
},
|
panel: (
|
||||||
permissions: ['code_repo_edit']
|
<Container padding={'large'}>
|
||||||
|
<GeneralSpaceSettings />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[space]
|
|
||||||
)
|
|
||||||
const permDeleteResult = hooks?.usePermissionTranslate?.(
|
|
||||||
{
|
{
|
||||||
resource: {
|
id: SettingsTab.labels,
|
||||||
resourceType: 'CODE_REPOSITORY'
|
title: getString('labels.labels'),
|
||||||
},
|
panel: <LabelsListing activeTab={activeTab} space={space} currentPageScope={LabelsPageScope.SPACE} />
|
||||||
permissions: ['code_repo_delete']
|
}
|
||||||
},
|
]
|
||||||
[space]
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.mainCtn}>
|
<Container className={css.main}>
|
||||||
<Page.Header title={getString('spaceSetting.settings')} />
|
<Page.Header title={getString('spaceSetting.settings')} />
|
||||||
<Page.Body>
|
<PageBody>
|
||||||
<Container padding="xlarge">
|
<Container className={cx(css.main, css.tabsContainer)}>
|
||||||
<Formik
|
<Tabs
|
||||||
formName="spaceGeneralSettings"
|
id="SpaceSettingsTabs"
|
||||||
initialValues={{
|
large={false}
|
||||||
name: data?.identifier,
|
defaultSelectedTabId={activeTab}
|
||||||
desc: data?.description
|
animate={false}
|
||||||
}}
|
onChange={(id: string) => {
|
||||||
onSubmit={voidFn(() => {
|
setActiveTab(id)
|
||||||
// @typescript-eslint/no-empty-function
|
history.replace(
|
||||||
})}>
|
routes.toCODESpaceSettings({
|
||||||
{formik => {
|
space: space as string,
|
||||||
return (
|
settingSection: id !== SpaceSettingsTab.general ? (id as string) : ''
|
||||||
<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>
|
tabList={tabListArray}></Tabs>
|
||||||
</Container>
|
</Container>
|
||||||
</Page.Body>
|
</PageBody>
|
||||||
</Container>
|
</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))
|
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()
|
return res.text()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,13 @@ export enum SettingsTab {
|
|||||||
webhooks = 'webhook',
|
webhooks = 'webhook',
|
||||||
general = '/',
|
general = '/',
|
||||||
branchProtection = 'rules',
|
branchProtection = 'rules',
|
||||||
security = 'security'
|
security = 'security',
|
||||||
|
labels = 'labels'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpaceSettingsTab {
|
||||||
|
general = '/',
|
||||||
|
labels = 'labels'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VulnerabilityScanningType {
|
export enum VulnerabilityScanningType {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* 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 { get } from 'lodash-es'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import langMap from 'lang-map'
|
import langMap from 'lang-map'
|
||||||
@ -28,7 +28,10 @@ import type {
|
|||||||
TypesRuleViolations,
|
TypesRuleViolations,
|
||||||
TypesViolation,
|
TypesViolation,
|
||||||
TypesCodeOwnerEvaluationEntry,
|
TypesCodeOwnerEvaluationEntry,
|
||||||
TypesListCommitResponse
|
TypesListCommitResponse,
|
||||||
|
RepoRepositoryOutput,
|
||||||
|
TypesLabel,
|
||||||
|
TypesLabelValue
|
||||||
} from 'services/code'
|
} from 'services/code'
|
||||||
import type { GitInfoProps } from './GitUtils'
|
import type { GitInfoProps } from './GitUtils'
|
||||||
|
|
||||||
@ -49,6 +52,14 @@ export enum FeatureType {
|
|||||||
RELEASED = 'released'
|
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 LIST_FETCHING_LIMIT = 20
|
||||||
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
|
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
|
||||||
export const DEFAULT_BRANCH_NAME = 'main'
|
export const DEFAULT_BRANCH_NAME = 'main'
|
||||||
@ -747,3 +758,122 @@ export function removeSpecificTextOptimized(
|
|||||||
viewRef?.current?.dispatch({ changes })
|
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