Skip to content

Commit bd7d3d2

Browse files
Merge pull request #1269 from devtron-labs/feat(scoped-variables)/floater-suggestions
Feat(scoped variables): floater suggestions
2 parents e23c616 + ea1acd7 commit bd7d3d2

32 files changed

+1185
-77
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"homepage": "/dashboard",
66
"dependencies": {
7-
"@devtron-labs/devtron-fe-common-lib": "0.0.26",
7+
"@devtron-labs/devtron-fe-common-lib": "0.0.26-beta-3",
88
"@sentry/browser": "^7.3.1",
99
"@sentry/integrations": "^7.3.1",
1010
"@sentry/tracing": "^7.3.1",
@@ -31,6 +31,7 @@
3131
"react-csv": "^2.2.2",
3232
"react-dates": "^21.8.0",
3333
"react-dom": "^17.0.2",
34+
"react-draggable": "^4.4.5",
3435
"react-ga4": "^1.4.1",
3536
"react-gtm-module": "^2.0.11",
3637
"react-keybind": "^0.9.4",

src/assets/icons/ic-variable.svg

Lines changed: 4 additions & 0 deletions
Loading

src/components/CIPipelineN/CIPipeline.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import React, { useState, useEffect, useMemo, useRef } from 'react'
22
import { NavLink } from 'react-router-dom'
33
import { Redirect, Route, Switch, useParams, useRouteMatch, useLocation } from 'react-router'
4-
import { ButtonWithLoader, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../common'
4+
import {
5+
ButtonWithLoader,
6+
FloatingVariablesSuggestions,
7+
importComponentFromFELibrary,
8+
sortObjectArrayAlphabetically,
9+
} from '../common'
510
import {
611
ServerErrors,
712
showError,
@@ -37,7 +42,7 @@ import {
3742
import { toast } from 'react-toastify'
3843
import { ValidationRules } from '../ciPipeline/validationRules'
3944
import { CIBuildType, CIPipelineDataType, CIPipelineType } from '../ciPipeline/types'
40-
import { ReactComponent as Close } from '../../assets/icons/ic-close.svg'
45+
import { ReactComponent as Close } from '../../assets/icons/ic-cross.svg'
4146
import Tippy from '@tippyjs/react'
4247
import { PreBuild } from './PreBuild'
4348
import { Sidebar } from './Sidebar'
@@ -189,6 +194,7 @@ export default function CIPipeline({
189194
list.push({
190195
id: 0,
191196
clusterName: '',
197+
clusterId: null,
192198
name: DEFAULT_ENV,
193199
active: false,
194200
isClusterActive: false,
@@ -199,6 +205,7 @@ export default function CIPipeline({
199205
list.push({
200206
id: env.id,
201207
clusterName: env.cluster_name,
208+
clusterId: env.cluster_id,
202209
name: env.environment_name,
203210
active: false,
204211
isClusterActive: env.isClusterActive,
@@ -512,7 +519,7 @@ export default function CIPipeline({
512519
}
513520

514521
let _ciPipeline = ciPipeline
515-
if(selectedEnv && selectedEnv.id !== 0) {
522+
if (selectedEnv && selectedEnv.id !== 0) {
516523
_ciPipeline.environmentId = selectedEnv.id
517524
} else {
518525
_ciPipeline.environmentId = undefined
@@ -692,6 +699,7 @@ export default function CIPipeline({
692699
<h2 className="fs-16 fw-6 lh-1-43 m-0" data-testid="build-pipeline-heading">
693700
{title}
694701
</h2>
702+
695703
<button
696704
type="button"
697705
className="dc__transparent flex icon-dim-24"
@@ -702,6 +710,7 @@ export default function CIPipeline({
702710
<Close className="icon-dim-24" />
703711
</button>
704712
</div>
713+
705714
{isAdvanced && (
706715
<ul className="ml-20 tab-list w-90">
707716
{isJobView ? (
@@ -719,9 +728,7 @@ export default function CIPipeline({
719728
</ul>
720729
)}
721730
<hr className="divider m-0" />
722-
<pipelineContext.Provider
723-
value={contextValue}
724-
>
731+
<pipelineContext.Provider value={contextValue}>
725732
<div className={`ci-pipeline-advance ${isAdvanced ? 'pipeline-container' : ''}`}>
726733
{isAdvanced && (
727734
<div className="sidebar-container">
@@ -808,10 +815,31 @@ export default function CIPipeline({
808815
)
809816
}
810817

818+
const renderFloatingVariablesWidget = () => {
819+
if (!window._env_.ENABLE_SCOPED_VARIABLES || activeStageName === BuildStageVariable.Build) return <></>
820+
821+
return (
822+
<div className="flexbox">
823+
<div className="floating-scoped-variables-widget">
824+
<FloatingVariablesSuggestions
825+
zIndex={21}
826+
appId={appId}
827+
envId={selectedEnv?.id ? String(selectedEnv.id) : null}
828+
clusterId={selectedEnv?.clusterId}
829+
/>
830+
</div>
831+
</div>
832+
)
833+
}
834+
811835
return ciPipelineId || isAdvanced ? (
812-
<Drawer position="right" width="75%" minWidth="1024px" maxWidth="1200px">
813-
{renderCIPipelineModal()}
814-
</Drawer>
836+
<>
837+
{renderFloatingVariablesWidget()}
838+
839+
<Drawer position="right" width="75%" minWidth="1024px" maxWidth="1200px">
840+
{renderCIPipelineModal()}
841+
</Drawer>
842+
</>
815843
) : (
816844
<VisibleModal className="">{renderCIPipelineModal()}</VisibleModal>
817845
)

src/components/cdPipeline/NewCDPipeline.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
1717
import { ReactComponent as Close } from '../../assets/icons/ic-close.svg'
1818
import { NavLink, Redirect, Route, Switch, useParams, useRouteMatch } from 'react-router-dom'
1919
import { CDDeploymentTabText, DELETE_ACTION, SourceTypeMap, TriggerType, ViewType } from '../../config'
20-
import { ButtonWithLoader, sortObjectArrayAlphabetically } from '../common'
20+
import { ButtonWithLoader, FloatingVariablesSuggestions, sortObjectArrayAlphabetically } from '../common'
2121
import BuildCD from './BuildCD'
2222
import { CD_PATCH_ACTION, Environment, GeneratedHelmPush } from './cdPipeline.types'
2323
import {
@@ -121,6 +121,7 @@ export default function NewCDPipeline({
121121
isClusterCdActive: false,
122122
deploymentAppCreated: false,
123123
clusterName: '',
124+
clusterId: null,
124125
runPreStageInEnv: false,
125126
runPostStageInEnv: false,
126127
allowedDeploymentTypes: [],
@@ -249,6 +250,7 @@ export default function NewCDPipeline({
249250
list = list.map((env) => {
250251
return {
251252
id: env.id,
253+
clusterId: env.cluster_id,
252254
clusterName: env.cluster_name,
253255
name: env.environment_name,
254256
namespace: env.namespace || '',
@@ -322,7 +324,10 @@ export default function NewCDPipeline({
322324
validateStage(BuildStageVariable.PostBuild, result.form)
323325
setIsAdvanced(true)
324326
setIsVirtualEnvironment(pipelineConfigFromRes.isVirtualEnvironment)
325-
setFormData(form)
327+
setFormData({
328+
...form,
329+
clusterId: result.form?.clusterId,
330+
})
326331
setPageState(ViewType.FORM)
327332
})
328333
.catch((error: ServerErrors) => {
@@ -1090,10 +1095,31 @@ export default function NewCDPipeline({
10901095
)
10911096
}
10921097

1098+
const renderFloatingVariablesWidget = () => {
1099+
if (!window._env_.ENABLE_SCOPED_VARIABLES || activeStageName === BuildStageVariable.Build) return <></>
1100+
1101+
return (
1102+
<div className="flexbox">
1103+
<div className="floating-scoped-variables-widget">
1104+
<FloatingVariablesSuggestions
1105+
zIndex={21}
1106+
appId={appId}
1107+
envId={formData?.environmentId ? String(formData.environmentId) : null}
1108+
clusterId={formData?.clusterId}
1109+
/>
1110+
</div>
1111+
</div>
1112+
)
1113+
}
1114+
10931115
return cdPipelineId || isAdvanced ? (
1094-
<Drawer position="right" width="75%" minWidth="1024px" maxWidth="1200px">
1095-
{renderCDPipelineModal()}
1096-
</Drawer>
1116+
<>
1117+
{renderFloatingVariablesWidget()}
1118+
1119+
<Drawer position="right" width="75%" minWidth="1024px" maxWidth="1200px">
1120+
{renderCDPipelineModal()}
1121+
</Drawer>
1122+
</>
10971123
) : (
10981124
<VisibleModal className="">{renderCDPipelineModal()}</VisibleModal>
10991125
)

src/components/cdPipeline/cdPipeline.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,16 @@ export async function getCDPipelineConfig(appId: string, pipelineId: string): Pr
5858
active: envId == env.id,
5959
isClusterCdActive: env.isClusterCdActive,
6060
allowedDeploymentTypes: env.allowedDeploymentTypes || [],
61+
clusterId: env.cluster_id,
6162
}
6263
});
6364

64-
let env = environments.find((e) => e.id === cdPipeline.environmentId)
65+
const env = environments.find((e) => e.id === cdPipeline.environmentId)
6566

6667
const form = {
6768
name: cdPipeline.name,
6869
environmentId: cdPipeline.environmentId,
70+
clusterId: env.clusterId,
6971
namespace: env.namespace,
7072
triggerType: cdPipeline.isManual ? TriggerType.Manual : TriggerType.Auto,
7173
preBuildStage: cdPipeline.preDeployStage || { id: 0, triggerType: TriggerType.Auto, steps: [] },

src/components/cdPipeline/cdPipeline.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface Environment {
5454
namespace: string
5555
active: boolean
5656
clusterName: string
57+
clusterId: string
5758
isClusterCdActive: boolean
5859
isVirtualEnvironment?: boolean
5960
allowedDeploymentTypes?: DeploymentAppTypes[]
@@ -215,6 +216,7 @@ export interface CDFormType {
215216
}
216217
isClusterCdActive: boolean
217218
deploymentAppCreated: boolean,
219+
clusterId: string,
218220
clusterName: string
219221
runPreStageInEnv: boolean,
220222
runPostStageInEnv: boolean,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useState, useEffect, useCallback } from 'react'
2+
import Tippy from '@tippyjs/react'
3+
import { copyToClipboard } from '../helpers/Helpers'
4+
import ClipboardProps from './types'
5+
import { ReactComponent as ICCopy } from '../../../assets/icons/ic-copy.svg'
6+
7+
/**
8+
* @param content - Content to be copied
9+
* @param copiedTippyText - Text to be shown in the tippy when the content is copied
10+
* @param duration - Duration for which the tippy should be shown
11+
* @param trigger - To trigger the copy action, if set to true the content will be copied, use case being triggering the copy action from outside the component
12+
* @param setTrigger - Callback function to set the trigger
13+
*/
14+
export default function ClipboardButton({ content, copiedTippyText, duration, trigger, setTrigger }: ClipboardProps) {
15+
const [copied, setCopied] = useState<boolean>(false)
16+
const [enableTippy, setEnableTippy] = useState<boolean>(false)
17+
18+
const handleTextCopied = () => setCopied(true)
19+
const handleEnableTippy = () => setEnableTippy(true)
20+
const handleDisableTippy = () => setEnableTippy(false)
21+
const handleCopyContent = useCallback(() => copyToClipboard(content, handleTextCopied), [content])
22+
23+
useEffect(() => {
24+
if (!copied) return
25+
26+
const timeout = setTimeout(() => {
27+
setCopied(false)
28+
setTrigger(false)
29+
}, duration)
30+
31+
return () => clearTimeout(timeout)
32+
}, [copied, duration, setTrigger])
33+
34+
useEffect(() => {
35+
if (!trigger) return
36+
37+
setCopied(true)
38+
handleCopyContent()
39+
}, [trigger, handleCopyContent])
40+
41+
return (
42+
<div className="icon-dim-16 flex center">
43+
<Tippy
44+
className="default-tt"
45+
content={copied ? copiedTippyText : 'Copy'}
46+
placement="right"
47+
visible={copied || enableTippy}
48+
>
49+
<button
50+
type="button"
51+
className="dc__hover-n100 dc__outline-none-imp p-0 flex bcn-0 dc__no-border"
52+
onMouseEnter={handleEnableTippy}
53+
onMouseLeave={handleDisableTippy}
54+
onClick={handleCopyContent}
55+
>
56+
<ICCopy className="icon-dim-16" />
57+
</button>
58+
</Tippy>
59+
</div>
60+
)
61+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react'
2+
import { render, screen, fireEvent } from '@testing-library/react'
3+
import ClipboardButton from '../ClipboardButton'
4+
5+
jest.mock('../../helpers/Helpers', () => ({
6+
copyToClipboard: jest.fn().mockImplementation((content, callback) => {
7+
callback()
8+
}),
9+
}))
10+
11+
describe('When ClipboardButton mounts', () => {
12+
beforeEach(() => {
13+
jest.clearAllMocks()
14+
})
15+
16+
it('should show the copy icon', () => {
17+
render(
18+
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
19+
)
20+
expect(screen.getByRole('button')).toBeTruthy()
21+
})
22+
23+
it('should show tippy on hover', () => {
24+
render(
25+
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
26+
)
27+
fireEvent.mouseEnter(screen.getByRole('button'))
28+
expect(screen.getByText('Copy')).toBeTruthy()
29+
})
30+
31+
it('should show copiedTippyText when trigger is true', () => {
32+
render(<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger setTrigger={null} />)
33+
expect(screen.getByText('test')).toBeTruthy()
34+
})
35+
36+
it('should call copyToClipboard when clicked', () => {
37+
render(
38+
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
39+
)
40+
fireEvent.click(screen.getByRole('button'))
41+
expect(screen.getByText('test')).toBeTruthy()
42+
})
43+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default interface ClipboardProps {
2+
content: string
3+
copiedTippyText: string
4+
duration: number
5+
trigger: boolean
6+
setTrigger: React.Dispatch<React.SetStateAction<boolean>>
7+
}

0 commit comments

Comments
 (0)