Skip to content

Commit b2d24c1

Browse files
Merge pull request #203 from devtron-labs/feat/mandatory-scoped-variables
feat: mandatory scoped variables
2 parents 1c091c8 + 515e366 commit b2d24c1

File tree

17 files changed

+535
-9
lines changed

17 files changed

+535
-9
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const URLS = {
5959
APP_TRIGGER: 'trigger',
6060
GLOBAL_CONFIG_DOCKER: '/global-config/docker',
6161
DEPLOYMENT_HISTORY_CONFIGURATIONS: '/configuration',
62+
GLOBAL_CONFIG_SCOPED_VARIABLES: '/global-config/scoped-variables',
6263
}
6364

6465
export const ROUTES = {

src/Common/CustomTagSelector/ResizableTagTextArea.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export const ResizableTagTextArea = ({
3232
dependentRef,
3333
dataTestId,
3434
handleKeyDown,
35+
disabled,
36+
disableOnBlurResizeToMinHeight,
3537
}: ResizableTagTextAreaProps) => {
3638
const [text, setText] = useState('')
3739

@@ -41,7 +43,7 @@ export const ResizableTagTextArea = ({
4143

4244
const handleChange = (event) => {
4345
setText(event.target.value)
44-
onChange(event)
46+
onChange?.(event)
4547
}
4648

4749
const reInitHeight = () => {
@@ -69,9 +71,11 @@ export const ResizableTagTextArea = ({
6971
useThrottledEffect(reInitHeight, 500, [text])
7072

7173
const handleOnBlur = (event) => {
72-
refVar.current.style.height = `${minHeight}px`
73-
if (dependentRef) {
74-
dependentRef.current.style.height = `${minHeight}px`
74+
if (!disableOnBlurResizeToMinHeight) {
75+
refVar.current.style.height = `${minHeight}px`
76+
if (dependentRef) {
77+
dependentRef.current.style.height = `${minHeight}px`
78+
}
7579
}
7680
onBlur && onBlur(event)
7781
}
@@ -95,6 +99,7 @@ export const ResizableTagTextArea = ({
9599
tabIndex={tabIndex}
96100
data-testid={dataTestId}
97101
onKeyDown={handleKeyDown}
102+
disabled={disabled}
98103
/>
99104
)
100105
}

src/Common/CustomTagSelector/Types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,6 @@ export interface ResizableTagTextAreaProps {
8585
dependentRef?: React.MutableRefObject<HTMLTextAreaElement>
8686
dataTestId?: string
8787
handleKeyDown?: any
88+
disabled?: boolean
89+
disableOnBlurResizeToMinHeight?: boolean
8890
}

src/Common/Types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ export interface RadioInterface {
235235
showTippy?: boolean
236236
tippyContent?: any
237237
tippyPlacement?: string
238+
/**
239+
* If false would make radio group controlled
240+
*/
238241
canSelect?: boolean
239242
isDisabled?: boolean
240243
tippyClass?: string
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export enum ScopedVariablesFileViewType {
2+
/**
3+
* Used to show yaml editor for editing/creating variables
4+
*/
5+
YAML = 'yaml',
6+
/**
7+
* Shows the variable list view
8+
*/
9+
SAVED = 'variables',
10+
/**
11+
* Shows the variables in environment list view
12+
*/
13+
ENVIRONMENT_LIST = 'environments',
14+
}
15+
16+
export interface SavedVariablesViewParamsType {
17+
currentView: ScopedVariablesFileViewType
18+
}

src/Pages/GlobalConfigurations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './BuildInfra'
1818
export * from './Authorization'
19+
export * from './ScopedVariables'
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright (c) 2024 Devtron Inc.
3+
* All rights reserved.
4+
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import React, { createRef, useEffect, useRef, useState } from 'react'
19+
20+
import { ReactComponent as ICArrowDown } from '../../../Assets/Icon/ic-sort-arrow-down.svg'
21+
import { ReactComponent as ICCross } from '../../../Assets/Icon/ic-cross.svg'
22+
import { ResizableTagTextArea, SortingOrder, useStateFilters } from '../../../Common'
23+
import { DEFAULT_SECRET_PLACEHOLDER } from '../../constants'
24+
import { stringComparatorBySortOrder } from '../../Helpers'
25+
import { KeyValueRow, KeyValueTableProps } from './KeyValueTable.types'
26+
27+
import './KeyValueTable.scss'
28+
29+
export const KeyValueTable = <K extends string>({
30+
config,
31+
maskValue,
32+
isSortable,
33+
headerComponent,
34+
onChange,
35+
onDelete,
36+
placeholder,
37+
isAdditionNotAllowed,
38+
}: KeyValueTableProps<K>) => {
39+
// CONSTANTS
40+
const { headers, rows } = config
41+
const firstHeaderKey = headers[0].key
42+
const secondHeaderKey = headers[1].key
43+
44+
// STATES
45+
const [updatedRows, setUpdatedRows] = useState<KeyValueRow<K>[]>(rows)
46+
const [newRowAdded, setNewRowAdded] = useState(false)
47+
48+
// HOOKS
49+
const { sortBy, sortOrder, handleSorting } = useStateFilters({
50+
initialSortKey: firstHeaderKey,
51+
})
52+
const inputRowRef = useRef<HTMLTextAreaElement>()
53+
const keyTextAreaRef = useRef<Record<string, React.RefObject<HTMLTextAreaElement>>>()
54+
const valueTextAreaRef = useRef<Record<string, React.RefObject<HTMLTextAreaElement>>>()
55+
56+
if (!keyTextAreaRef.current) {
57+
keyTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {})
58+
}
59+
60+
if (!valueTextAreaRef.current) {
61+
valueTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {})
62+
}
63+
64+
useEffect(() => {
65+
const sortedRows = [...updatedRows]
66+
sortedRows.sort((a, b) => stringComparatorBySortOrder(a.data[sortBy].value, b.data[sortBy].value, sortOrder))
67+
setUpdatedRows(sortedRows)
68+
}, [sortOrder])
69+
70+
useEffect(() => {
71+
const firstRow = updatedRows?.[0]
72+
if (firstRow && newRowAdded) {
73+
setNewRowAdded(false)
74+
75+
if (
76+
!firstRow.data[secondHeaderKey].value &&
77+
keyTextAreaRef.current[firstRow.id].current &&
78+
valueTextAreaRef.current[firstRow.id].current
79+
) {
80+
keyTextAreaRef.current[firstRow.id].current.focus()
81+
}
82+
if (
83+
!firstRow.data[firstHeaderKey].value &&
84+
keyTextAreaRef.current[firstRow.id].current &&
85+
valueTextAreaRef.current[firstRow.id].current
86+
) {
87+
valueTextAreaRef.current[firstRow.id].current.focus()
88+
}
89+
}
90+
}, [newRowAdded])
91+
92+
// METHODS
93+
const onSortBtnClick = () => handleSorting(sortBy)
94+
95+
const onNewRowAdd = (key: K) => (e: React.ChangeEvent<HTMLTextAreaElement>) => {
96+
const { value } = e.target
97+
98+
const id = (Date.now() * Math.random()).toString(16)
99+
const data = {
100+
data: {
101+
[firstHeaderKey]: {
102+
value: key === firstHeaderKey ? value : '',
103+
},
104+
[secondHeaderKey]: {
105+
value: key === secondHeaderKey ? value : '',
106+
},
107+
},
108+
id,
109+
} as KeyValueRow<K>
110+
111+
setNewRowAdded(true)
112+
setUpdatedRows([data, ...updatedRows])
113+
onChange?.(id, key, value)
114+
115+
keyTextAreaRef.current = {
116+
...keyTextAreaRef.current,
117+
[id]: createRef(),
118+
}
119+
valueTextAreaRef.current = {
120+
...valueTextAreaRef.current,
121+
[id]: createRef(),
122+
}
123+
}
124+
125+
const onRowDelete = (row: KeyValueRow<K>) => () => {
126+
const remainingRows = updatedRows.filter(({ id }) => id !== row.id)
127+
setUpdatedRows(remainingRows)
128+
129+
delete keyTextAreaRef.current[row.id]
130+
delete valueTextAreaRef.current[row.id]
131+
132+
onDelete?.(row.id)
133+
}
134+
135+
const onRowDataEdit = (row: KeyValueRow<K>, key: K) => (e: React.ChangeEvent<HTMLTextAreaElement>) => {
136+
const { value } = e.target
137+
138+
if (!value && !row.data[key === firstHeaderKey ? secondHeaderKey : firstHeaderKey].value) {
139+
onRowDelete(row)()
140+
141+
if (inputRowRef.current) {
142+
inputRowRef.current.focus()
143+
}
144+
} else {
145+
const rowData = {
146+
...row,
147+
data: {
148+
...row.data,
149+
[key]: {
150+
...row.data[key],
151+
value,
152+
},
153+
},
154+
}
155+
const editedRows = updatedRows.map((_row) => (_row.id === row.id ? rowData : _row))
156+
setUpdatedRows(editedRows)
157+
}
158+
}
159+
160+
const onRowDataBlur = (row: KeyValueRow<K>, key: K) => (e: React.FocusEvent<HTMLTextAreaElement>) => {
161+
const { value } = e.target
162+
163+
if (value || row.data[key === firstHeaderKey ? secondHeaderKey : firstHeaderKey].value) {
164+
onChange?.(row.id, key, value)
165+
}
166+
}
167+
168+
return (
169+
<div className="dc__border br-4 w-100 bcn-0 key-value">
170+
<div
171+
className={`key-value__row flexbox dc__align-items-center bcn-50 ${!isAdditionNotAllowed || updatedRows.length ? 'dc__border-bottom-n1' : ''}`}
172+
>
173+
{headers.map(({ key, label, className }) =>
174+
isSortable && key === firstHeaderKey ? (
175+
<button
176+
key={key}
177+
type="button"
178+
className={`dc__unset-button-styles cn-9 fs-13 lh-20 py-8 px-12 fw-6 flexbox dc__align-items-center dc__gap-2 dc__align-self-stretch key-value__header__col-1 ${className || ''}`}
179+
onClick={onSortBtnClick}
180+
>
181+
{label}
182+
<ICArrowDown
183+
className="icon-dim-16 scn-7 rotate cursor"
184+
style={{
185+
['--rotateBy' as string]: sortOrder === SortingOrder.ASC ? '0deg' : '180deg',
186+
}}
187+
/>
188+
</button>
189+
) : (
190+
<div
191+
key={key}
192+
className={`cn-9 fs-13 lh-20 py-8 px-12 fw-6 flexbox dc__align-items-center dc__gap-2 ${key === firstHeaderKey ? 'dc__align-self-stretch dc__border-right--n1 key-value__header__col-1' : 'flex-grow-1 dc__border-left-n1'} ${className || ''}`}
193+
>
194+
{label}
195+
</div>
196+
),
197+
)}
198+
{!!headerComponent && <div className="px-12">{headerComponent}</div>}
199+
</div>
200+
{!isAdditionNotAllowed && (
201+
<div
202+
className={`key-value__row flexbox dc__align-items-center ${updatedRows.length ? 'dc__border-bottom-n1' : ''}`}
203+
>
204+
{headers.map(({ key }) => (
205+
<div
206+
key={key}
207+
className={`cn-9 fs-13 lh-20 py-8 px-12 flex dc__overflow-auto ${key === firstHeaderKey ? 'dc__align-self-stretch dc__border-right--n1 key-value__header__col-1' : 'flex-grow-1'}`}
208+
>
209+
<textarea
210+
ref={key === firstHeaderKey ? inputRowRef : undefined}
211+
className="key-value__row-input key-value__row-input--add placeholder-cn5 p-0 lh-20 fs-13 fw-4 dc__no-border-imp dc__no-border-radius"
212+
value=""
213+
rows={1}
214+
placeholder={placeholder[key]}
215+
onChange={onNewRowAdd(key)}
216+
/>
217+
</div>
218+
))}
219+
</div>
220+
)}
221+
{updatedRows.map((row, index) => (
222+
<div
223+
key={row.id}
224+
className={`key-value__row flexbox dc__align-items-center ${index !== updatedRows.length - 1 ? 'dc__border-bottom-n1' : ''}`}
225+
>
226+
{headers.map(({ key }) => (
227+
<div
228+
key={key}
229+
className={`cn-9 fs-13 lh-20 px-12 dc__overflow-auto flexbox dc__align-items-center dc__gap-4 ${key === firstHeaderKey ? 'dc__align-self-stretch dc__border-right--n1 key-value__header__col-1' : 'flex-grow-1'}`}
230+
>
231+
{maskValue?.[key] && row.data[key].value ? (
232+
DEFAULT_SECRET_PLACEHOLDER
233+
) : (
234+
<>
235+
<ResizableTagTextArea
236+
{...row.data[key]}
237+
className="key-value__row-input placeholder-cn5 py-8 px-0 dc__no-border-imp dc__no-border-radius"
238+
minHeight={20}
239+
maxHeight={160}
240+
value={row.data[key].value}
241+
placeholder={placeholder[key]}
242+
onChange={onRowDataEdit(row, key)}
243+
onBlur={onRowDataBlur(row, key)}
244+
refVar={
245+
key === firstHeaderKey
246+
? keyTextAreaRef.current?.[row.id]
247+
: valueTextAreaRef.current?.[row.id]
248+
}
249+
dependentRef={
250+
key === firstHeaderKey
251+
? valueTextAreaRef.current?.[row.id]
252+
: keyTextAreaRef.current?.[row.id]
253+
}
254+
disableOnBlurResizeToMinHeight
255+
/>
256+
{row.data[key].required && (
257+
<span className="cr-5 fs-16 dc__align-self-start px-6 py-8">*</span>
258+
)}
259+
</>
260+
)}
261+
</div>
262+
))}
263+
<button
264+
type="button"
265+
className="dc__unset-button-styles dc__align-self-stretch dc__no-shrink flex py-10 px-8 dc__border-left-n1--important dc__hover-n50"
266+
onClick={onRowDelete(row)}
267+
>
268+
<ICCross aria-label="delete-row" className="icon-dim-16 fcn-4 dc__align-self-start cursor" />
269+
</button>
270+
</div>
271+
))}
272+
</div>
273+
)
274+
}

0 commit comments

Comments
 (0)