@devtron-labs/devtron-fe-common-lib 0.0.45 → 0.0.47-beta-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devtron-labs/devtron-fe-common-lib",
3
- "version": "0.0.45",
3
+ "version": "0.0.47-beta-3",
4
4
  "description": "Supporting common component library",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -20,25 +20,25 @@
20
20
  "homepage": "https://github.com/devtron-labs/devtron-fe-common-lib#readme",
21
21
  "devDependencies": {
22
22
  "@rollup/plugin-babel": "^6.0.3",
23
- "@rollup/plugin-node-resolve": "^15.0.1",
23
+ "@rollup/plugin-commonjs": "24.0.1",
24
24
  "@rollup/plugin-image": "3.0.1",
25
- "rollup-plugin-peer-deps-external": "^2.2.4",
26
- "rollup-plugin-postcss": "^4.0.2",
27
- "rollup-plugin-typescript2": "^0.34.1",
25
+ "@rollup/plugin-node-resolve": "^15.0.1",
26
+ "@rollup/plugin-url": "8.0.1",
27
+ "@sentry/browser": "^7.31.1",
28
+ "@svgr/rollup": "6.5.1",
29
+ "@tippyjs/react": "^4.2.0",
28
30
  "@types/react": "17.0.39",
29
31
  "@types/react-dom": "17.0.13",
30
32
  "@types/react-router-dom": "^5.3.3",
31
- "rollup": "^3.10.0",
32
- "rollup-plugin-delete": "^2.0.0",
33
- "@svgr/rollup": "6.5.1",
34
- "@rollup/plugin-url": "8.0.1",
35
- "@rollup/plugin-commonjs": "24.0.1",
36
33
  "moment": "^2.29.4",
37
- "typescript": "^4.9.4",
38
- "react-toastify": "^8.2.0",
39
- "@tippyjs/react": "^4.2.0",
40
34
  "react-select": "5.2.2",
41
- "@sentry/browser": "^7.31.1"
35
+ "react-toastify": "^8.2.0",
36
+ "rollup": "^3.10.0",
37
+ "rollup-plugin-delete": "^2.0.0",
38
+ "rollup-plugin-peer-deps-external": "^2.2.4",
39
+ "rollup-plugin-postcss": "^4.0.2",
40
+ "rollup-plugin-typescript2": "^0.34.1",
41
+ "typescript": "^4.9.4"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@rjsf/core": "^5.13.3",
@@ -50,6 +50,8 @@
50
50
  "react-router-dom": "^5.3.0"
51
51
  },
52
52
  "dependencies": {
53
+ "fast-json-patch": "^3.1.1",
54
+ "jsonpath-plus": "^7.2.0",
53
55
  "sass": "^1.56.2",
54
56
  "tslib": "^2.4.1"
55
57
  }
@@ -0,0 +1,78 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { fetchChartTemplateVersions } from './Common.service'
3
+ import { ChartVersionAndTypeSelectorProps, DeploymentChartVersionType } from './Types'
4
+ import ReactSelect from 'react-select'
5
+ import { customStyles, getFilteredChartVersions, showError } from './Helper'
6
+
7
+ // @TODO: Generalize this component to be used in CodeEditor as Chart selector toolbar
8
+ // when the Code Editor is moved to the fe-common-lib
9
+ const ChartVersionAndTypeSelector = ({ setSelectedChartRefId }: ChartVersionAndTypeSelectorProps) => {
10
+ const [charts, setCharts] = useState<DeploymentChartVersionType[]>([])
11
+ const [selectedChartType, setSelectedChartType] = useState(null)
12
+ const [chartVersionOptions, setChartVersionOptions] = useState([])
13
+ const [chartTypeOptions, setChartTypeOptions] = useState([])
14
+ const [selectedChartVersion, setSelectedChartVersion] = useState(null)
15
+
16
+ useEffect(() => {
17
+ fetchChartTemplateVersions()
18
+ .then((res) => {
19
+ const charts = res?.result || []
20
+ setCharts(charts)
21
+ // Extract unique chart types from the data
22
+ const chartTypeOptions = [...new Set(charts.map((item) => item.chartType))].map((type) => ({
23
+ value: type,
24
+ label: type,
25
+ }))
26
+ setChartTypeOptions(chartTypeOptions)
27
+ const filteredVersions = getFilteredChartVersions(charts, chartTypeOptions[0])
28
+ selectFirstChartVersion(filteredVersions)
29
+ })
30
+ .catch((err) => {
31
+ showError(err)
32
+ })
33
+ }, [])
34
+
35
+ const selectFirstChartVersion = (filteredVersions) => {
36
+ setChartVersionOptions(filteredVersions)
37
+ setSelectedChartVersion(filteredVersions[0]) // Select the first chart version by default
38
+ setSelectedChartRefId(filteredVersions[0]?.chartRefId)
39
+ }
40
+
41
+ // Function to update chart version options based on selected chart type
42
+ const handleChartTypeChange = (selectedOption) => {
43
+ setSelectedChartType(selectedOption)
44
+ const filteredVersions = getFilteredChartVersions(charts, selectedOption)
45
+ selectFirstChartVersion(filteredVersions)
46
+ }
47
+
48
+ // Function to handle the change of the selected chart version
49
+ const handleChartVersionChange = (selectedOption) => {
50
+ setSelectedChartVersion(selectedOption)
51
+ setSelectedChartRefId(selectedOption.chartRefId)
52
+ }
53
+
54
+ return (
55
+ <div className="flex">
56
+ <div className="chart-type-options flex" data-testid="chart-type-options">
57
+ <span className="chart-type-options-label mr-4">Chart Type</span>
58
+ <ReactSelect
59
+ value={selectedChartType ?? chartTypeOptions[0]}
60
+ options={chartTypeOptions}
61
+ onChange={handleChartTypeChange}
62
+ styles={customStyles}
63
+ />
64
+ </div>
65
+ <div className="chart-version-options flex" data-testid="chart-version-options">
66
+ <span className="chart-version-options-label mr-4">Chart Version</span>
67
+ <ReactSelect
68
+ value={selectedChartVersion ?? chartVersionOptions[0]}
69
+ options={chartVersionOptions}
70
+ onChange={handleChartVersionChange}
71
+ styles={customStyles}
72
+ />
73
+ </div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ export default ChartVersionAndTypeSelector
@@ -309,3 +309,7 @@ export function createGitCommitUrl(url: string, revision: string): string {
309
309
  }
310
310
  return "NA"
311
311
  }
312
+
313
+ export function fetchChartTemplateVersions() {
314
+ return get(`${ROUTES.DEPLOYMENT_TEMPLATE_LIST}?appId=-1&envId=-1`)
315
+ }
@@ -24,7 +24,8 @@ export const ROUTES = {
24
24
  PROJECT_LIST_MIN: 'team/autocomplete',
25
25
  USER_CHECK_ROLE: 'user/check/roles',
26
26
  IMAGE_TAGGING:'app/image-tagging',
27
- CD_MATERIAL_GET: 'app/cd-pipeline'
27
+ CD_MATERIAL_GET: 'app/cd-pipeline',
28
+ DEPLOYMENT_TEMPLATE_LIST: 'app/template/list'
28
29
  }
29
30
 
30
31
  export enum KEY_VALUE {
@@ -0,0 +1,131 @@
1
+ import { CustomInputProps } from './Types'
2
+ import { ReactComponent as Info } from '../../Assets/Icon/ic-info-filled.svg'
3
+ import { ReactComponent as ErrorIcon } from '../../Assets/Icon/ic-warning.svg'
4
+
5
+ export function CustomInput({
6
+ name,
7
+ value,
8
+ error,
9
+ onChange,
10
+ onFocus = (e) => {},
11
+ label,
12
+ type = 'text',
13
+ disabled = false,
14
+ labelClassName = '',
15
+ placeholder = '',
16
+ tabIndex = 1,
17
+ dataTestid = '',
18
+ isRequiredField = false,
19
+ autoFocus = false,
20
+ rootClassName = '',
21
+ autoComplete = 'off',
22
+ helperText = '',
23
+ handleOnBlur,
24
+ readOnly = false,
25
+ noTrim = false,
26
+ ref,
27
+ onKeyPress,
28
+ defaultValue,
29
+ onKeyDown,
30
+ required,
31
+ additionalErrorInfo
32
+ }: CustomInputProps) {
33
+
34
+ function handleError(error: any): any[] {
35
+ if (!Array.isArray(error)) {
36
+ return [error]
37
+ }
38
+ return error
39
+ }
40
+
41
+ const onBlur = (event) => {
42
+ //NOTE: This is to prevent the input from being trimmed when the user do not want to trim the input
43
+ if (!noTrim) {
44
+ event.stopPropagation()
45
+ event.target.value = event.target.value?.trim()
46
+ onChange(event)
47
+ }
48
+ if (typeof handleOnBlur === 'function') {
49
+ handleOnBlur(event)
50
+ }
51
+ }
52
+
53
+ const renderFormErrorWithIcon = (error: string) => {
54
+ return (
55
+ <div className="form__error" key={error}>
56
+ <ErrorIcon className="form__icon form__icon--error" />
57
+ {error}
58
+ {typeof additionalErrorInfo === 'function' && additionalErrorInfo()}
59
+ </div>
60
+ )
61
+ }
62
+
63
+ const getInputError = () => {
64
+ if (!error?.length) {
65
+ return null
66
+ } else {
67
+ if (typeof error === 'object') {
68
+ return handleError(error).map((err: string) => renderFormErrorWithIcon(err))
69
+ }
70
+ return renderFormErrorWithIcon(error)
71
+ }
72
+ }
73
+
74
+ const renderInputLabelConditionally = () => {
75
+ if (typeof label === 'string') {
76
+ return <span className={`${isRequiredField ? 'dc__required-field' : ''}`}>{label}</span>
77
+ } else {
78
+ return label()
79
+ }
80
+ }
81
+
82
+ const renderInputLabel = () => {
83
+ if (!label) {
84
+ return null
85
+ } else {
86
+ return (
87
+ <label className={`form__label ${labelClassName}`} data-testid={`label-${dataTestid}`}>
88
+ {renderInputLabelConditionally()}
89
+ </label>
90
+ )
91
+ }
92
+ }
93
+
94
+ return (
95
+ <div className="flex column left top">
96
+ {renderInputLabel()}
97
+ <input
98
+ data-testid={dataTestid}
99
+ type={type}
100
+ name={name}
101
+ autoComplete={autoComplete}
102
+ className={`form__input ${rootClassName}`}
103
+ onChange={(e) => {
104
+ e.persist()
105
+ onChange(e)
106
+ }}
107
+ onBlur={onBlur}
108
+ onFocus={onFocus}
109
+ placeholder={placeholder}
110
+ value={value}
111
+ disabled={disabled}
112
+ tabIndex={tabIndex}
113
+ autoFocus={autoFocus}
114
+ readOnly={readOnly}
115
+ ref={ref}
116
+ onKeyPress={onKeyPress}
117
+ defaultValue={defaultValue}
118
+ onKeyDown={onKeyDown}
119
+ required={required}
120
+ />
121
+
122
+ {getInputError()}
123
+ {helperText && (
124
+ <div className="form__text-field-info">
125
+ <Info className="form__icon form__icon--info" />
126
+ <p className="sentence-case">{helperText}</p>
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }
@@ -0,0 +1,30 @@
1
+ import React, { HTMLInputTypeAttribute } from 'react'
2
+
3
+ export interface CustomInputProps {
4
+ name: string
5
+ value: string | number
6
+ onChange: (e: any) => void
7
+ onFocus?: (e: any) => void
8
+ autoComplete?: string
9
+ label?: string | (() => React.ReactNode)
10
+ labelClassName?: string
11
+ type?: HTMLInputTypeAttribute
12
+ disabled?: boolean
13
+ placeholder?: string
14
+ tabIndex?: number
15
+ dataTestid?: string
16
+ isRequiredField?: boolean
17
+ autoFocus?: boolean
18
+ rootClassName?: string
19
+ error?: string[] | string
20
+ helperText?: string
21
+ handleOnBlur?: (e) => void
22
+ readOnly?: boolean
23
+ noTrim?: boolean
24
+ ref?: React.LegacyRef<HTMLInputElement>
25
+ onKeyPress?: (e) => void
26
+ defaultValue?: string | number | ReadonlyArray<string> | undefined
27
+ onKeyDown?: (e) => void
28
+ required?: boolean
29
+ additionalErrorInfo?: React.ReactNode
30
+ }
@@ -0,0 +1,2 @@
1
+ export * from './CustomInput'
2
+ export * from './Types'
@@ -3,11 +3,12 @@ import moment from 'moment'
3
3
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { useLocation } from 'react-router-dom'
5
5
  import { toast } from 'react-toastify'
6
- import { ReactComponent as FormError } from '../Assets/Icon/ic-warning.svg'
7
6
  import { ERROR_EMPTY_SCREEN, TOKEN_COOKIE_NAME } from './Constants'
8
7
  import { ServerErrors } from './ServerError'
9
8
  import { toastAccessDenied } from './ToastBody'
10
9
  import { AsyncOptions, AsyncState, UseSearchString } from './Types'
10
+ import { JSONPath } from 'jsonpath-plus'
11
+ import * as jsonpatch from 'fast-json-patch'
11
12
 
12
13
  toast.configure({
13
14
  autoClose: 3000,
@@ -116,11 +117,11 @@ export function getRandomColor(email: string): string {
116
117
  return colors[sum % colors.length]
117
118
  }
118
119
 
119
- export const getAlphabetIcon = (str: string, intialAlphabetClassName?: string) => {
120
+ export const getAlphabetIcon = (str: string, rootClassName: string = "") => {
120
121
  if (!str) return null
121
122
  return (
122
123
  <span
123
- className={`${intialAlphabetClassName ? intialAlphabetClassName : ""} alphabet-icon__initial fs-13 icon-dim-20 flex cn-0 mr-8`}
124
+ className={`${rootClassName} alphabet-icon__initial fs-13 icon-dim-20 flex cn-0 mr-8`}
124
125
  style={{ backgroundColor: getRandomColor(str) }}
125
126
  >
126
127
  {str[0]}
@@ -277,64 +278,6 @@ export function useForm(stateSchema, validationSchema = {}, callback) {
277
278
  return { state, disable, handleOnChange, handleOnSubmit }
278
279
  }
279
280
 
280
- function handleError(error: any): any[] {
281
- if (!error) {
282
- return []
283
- }
284
-
285
- if (!Array.isArray(error)) {
286
- return [error]
287
- }
288
-
289
- return error
290
- }
291
-
292
- export function CustomInput({
293
- name,
294
- value,
295
- error,
296
- onChange,
297
- onBlur = (e) => {},
298
- onFocus = (e) => {},
299
- label,
300
- type = 'text',
301
- disabled = false,
302
- autoComplete = 'off',
303
- labelClassName = '',
304
- placeholder = '',
305
- tabIndex = 1,
306
- dataTestid = '',
307
- }) {
308
- return (
309
- <div className="flex column left top">
310
- <label className={`form__label ${labelClassName}`}>{label}</label>
311
- <input
312
- data-testid={dataTestid}
313
- type={type}
314
- name={name}
315
- autoComplete="off"
316
- className="form__input"
317
- onChange={(e) => {
318
- e.persist()
319
- onChange(e)
320
- }}
321
- onBlur={onBlur}
322
- onFocus={onFocus}
323
- placeholder={placeholder}
324
- value={value}
325
- disabled={disabled}
326
- tabIndex={tabIndex}
327
- />
328
- {handleError(error).map((err) => (
329
- <div className="form__error" key={err}>
330
- <FormError className="form__icon form__icon--error" />
331
- {err}
332
- </div>
333
- ))}
334
- </div>
335
- )
336
- }
337
-
338
281
  export function handleUTCTime(ts: string, isRelativeTime = false) {
339
282
  let timestamp = ''
340
283
  try {
@@ -517,3 +460,99 @@ export const getUrlWithSearchParams = (url: string, params: Record<string | numb
517
460
  * Custom exception logger function for logging errors to sentry
518
461
  */
519
462
  export const logExceptionToSentry = Sentry.captureException.bind(window)
463
+
464
+ export const customStyles = {
465
+ control: (base, state) => ({
466
+ ...base,
467
+ minHeight: '32px',
468
+ boxShadow: 'none',
469
+ border: 'none',
470
+ cursor: 'pointer',
471
+ background: 'transparent',
472
+ }),
473
+ indicatorSeparator: (base, state) => ({
474
+ ...base,
475
+ width: 0,
476
+ }),
477
+ valueContainer: (base, state) => ({
478
+ ...base,
479
+ padding: '0',
480
+ fontSize: '13px',
481
+ fontWeight: '600',
482
+ }),
483
+ dropdownIndicator: (base, state) => ({
484
+ ...base,
485
+ color: 'var(--N400)',
486
+ padding: '0 8px',
487
+ transition: 'all .2s ease',
488
+ transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : 'rotate(0deg)',
489
+ }),
490
+ }
491
+
492
+ export const getFilteredChartVersions = (charts, selectedChartType) => {
493
+ // Filter chart versions based on selected chart type
494
+ return charts
495
+ .filter((item) => item?.chartType === selectedChartType.value)
496
+ .map((item) => ({
497
+ value: item?.chartVersion,
498
+ label: item?.chartVersion,
499
+ chartRefId: item.chartRefId,
500
+ }))
501
+ }
502
+ function removeEmptyObjectKeysAndNullValues(obj) {
503
+ //it recursively removes empty object keys and array values that are null
504
+ for (let key in obj) {
505
+ if (Array.isArray(obj[key])) obj[key] = obj[key].filter((item) => item !== null)
506
+ if (obj[key].length === 0) {
507
+ // Check if the array is empty
508
+ delete obj[key] // Delete the key if the array is empty
509
+ } else if (obj[key] && typeof obj[key] === 'object') {
510
+ if (removeEmptyObjectKeysAndNullValues(obj[key])) {
511
+ delete obj[key]
512
+ }
513
+ } else if (obj[key] === null || obj[key] === undefined) {
514
+ delete obj[key]
515
+ }
516
+ }
517
+ return Object.keys(obj).length === 0
518
+ }
519
+
520
+ export function getUnlockedJSON(json, jsonPathArray) {
521
+ const jsonCopy = JSON.parse(JSON.stringify(json))
522
+ let patches = jsonPathArray.flatMap((jsonPath) => {
523
+ const pathsToRemove = JSONPath({ path: jsonPath, json: jsonCopy, resultType: 'all' })
524
+ return pathsToRemove.map((result) =>
525
+ Array.isArray(result.parent)
526
+ ? { op: 'replace', path: result.pointer, value: null }
527
+ : { op: 'remove', path: result.pointer },
528
+ )
529
+ })
530
+ let newDocument = jsonpatch.applyPatch(jsonCopy, patches).newDocument
531
+
532
+ removeEmptyObjectKeysAndNullValues(newDocument)
533
+ return newDocument
534
+ }
535
+
536
+ export function getLockedJSON(json, jsonPathArray: string[]) {
537
+ const jsonCopy = JSON.parse(JSON.stringify(json))
538
+ let resultJson = {}
539
+ jsonPathArray.forEach((jsonPath) => {
540
+ const elements = JSONPath({ path: jsonPath, json: jsonCopy, resultType: 'all' })
541
+ elements.forEach((element) => {
542
+ const pathArray: string[] = JSONPath.toPathArray(element.path)
543
+ const lastPath = pathArray.pop()
544
+ let current = resultJson
545
+ for (let i = 0; i < pathArray.length; i++) {
546
+ let key = isNaN(Number(pathArray[i])) ? pathArray[i] : parseInt(pathArray[i])
547
+ if (!current[key]) {
548
+ current[key] = isNaN(Number(pathArray[i + 1])) ? {} : []
549
+ }
550
+ current = current[key]
551
+ }
552
+ let key = isNaN(Number(lastPath)) ? lastPath : parseInt(lastPath)
553
+ current[key] = element.value
554
+ })
555
+ })
556
+ return resultJson['$']
557
+ }
558
+
@@ -79,7 +79,6 @@ export interface TippyCustomizedProps {
79
79
  documentationLink?: string
80
80
  documentationLinkText?: string
81
81
  children: React.ReactElement<any>
82
- followCursor?: boolean
83
82
  }
84
83
  export interface GenericEmptyStateType {
85
84
  title: ReactNode
@@ -624,3 +623,73 @@ export interface CiPipeline {
624
623
  isOffendingMandatoryPlugin?: boolean
625
624
  pipelineType?: string
626
625
  }
626
+
627
+ export interface DeploymentChartVersionType {
628
+ chartRefId: number
629
+ chartVersion: string
630
+ chartType: string
631
+ type: number
632
+ }
633
+
634
+ export interface ChartVersionAndTypeSelectorProps {
635
+ setSelectedChartRefId: React.Dispatch<React.SetStateAction<number>>
636
+ }
637
+
638
+ export interface InformationBarProps {
639
+ text: string
640
+ className?: string
641
+ children?: React.ReactNode
642
+ }
643
+
644
+ export interface CodeEditorInterface {
645
+ value?: string
646
+ lineDecorationsWidth?: number
647
+ responseType?: string
648
+ onChange?: (string) => void
649
+ onBlur?: () => void
650
+ onFocus?: () => void
651
+ children?: any
652
+ defaultValue?: string
653
+ mode?: 'json' | 'yaml' | 'shell' | 'dockerfile' | 'plaintext'
654
+ tabSize?: number
655
+ readOnly?: boolean
656
+ noParsing?: boolean
657
+ minHeight?: number
658
+ maxHeight?: number
659
+ inline?: boolean
660
+ height?: number | string
661
+ shebang?: string | JSX.Element
662
+ diffView?: boolean
663
+ loading?: boolean
664
+ customLoader?: JSX.Element
665
+ theme?: string
666
+ original?: string
667
+ focus?: boolean
668
+ validatorSchema?: any
669
+ isKubernetes?: boolean
670
+ cleanData?: boolean
671
+ chartVersion?: any
672
+ }
673
+
674
+ export interface CodeEditorComposition {
675
+ Header?: React.FC<any>
676
+ LanguageChanger?: React.FC<any>
677
+ ThemeChanger?: React.FC<any>
678
+ ValidationError?: React.FC<any>
679
+ Clipboard?: React.FC<any>
680
+ Warning?: React.FC<InformationBarProps>
681
+ ErrorBar?: React.FC<InformationBarProps>
682
+ Information?: React.FC<InformationBarProps>
683
+ }
684
+
685
+ export interface CodeEditorHeaderComposition {
686
+ LanguageChanger?: React.FC<any>;
687
+ ThemeChanger?: React.FC<any>;
688
+ ValidationError?: React.FC<any>;
689
+ Clipboard?: React.FC<any>;
690
+ }
691
+ export interface CodeEditorHeaderInterface {
692
+ children?: any;
693
+ className?: string
694
+ hideDefaultSplitHeader?: boolean;
695
+ }
@@ -42,3 +42,5 @@ export * from './Hooks'
42
42
  export { AppStatus } from './AppStatus'
43
43
  export * from './RJSF'
44
44
  export * from './DevtronProgressing'
45
+ export { default as ChartVersionAndTypeSelector } from './ChartVersionAndTypeSelector'
46
+ export * from './CustomInput'