@devtron-labs/devtron-fe-common-lib 0.0.50 → 0.0.51-beta-2

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.50",
3
+ "version": "0.0.51-beta-2",
4
4
  "description": "Supporting common component library",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -47,9 +47,12 @@
47
47
  "react": "^17.0.2",
48
48
  "react-dom": "^17.0.2",
49
49
  "react-router": "^5.3.0",
50
- "react-router-dom": "^5.3.0"
50
+ "react-router-dom": "^5.3.0",
51
+ "react-draggable": "^4.4.5"
51
52
  },
52
53
  "dependencies": {
54
+ "fast-json-patch": "^3.1.1",
55
+ "jsonpath-plus": "^7.2.0",
53
56
  "sass": "^1.56.2",
54
57
  "tslib": "^2.4.1"
55
58
  }
@@ -0,0 +1,8 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M10.3335 9.33325C10.8858 9.33325 11.3335 8.88554 11.3335 8.33325C11.3335 7.78097 10.8858 7.33325 10.3335 7.33325C9.78121 7.33325 9.3335 7.78097 9.3335 8.33325C9.3335 8.88554 9.78121 9.33325 10.3335 9.33325Z" fill="#596168"/>
3
+ <path d="M5.66675 9.33325C6.21903 9.33325 6.66675 8.88554 6.66675 8.33325C6.66675 7.78097 6.21903 7.33325 5.66675 7.33325C5.11446 7.33325 4.66675 7.78097 4.66675 8.33325C4.66675 8.88554 5.11446 9.33325 5.66675 9.33325Z" fill="#596168"/>
4
+ <path d="M10.3335 5.33325C10.8858 5.33325 11.3335 4.88554 11.3335 4.33325C11.3335 3.78097 10.8858 3.33325 10.3335 3.33325C9.78121 3.33325 9.3335 3.78097 9.3335 4.33325C9.3335 4.88554 9.78121 5.33325 10.3335 5.33325Z" fill="#596168"/>
5
+ <path d="M5.66675 5.33325C6.21903 5.33325 6.66675 4.88554 6.66675 4.33325C6.66675 3.78097 6.21903 3.33325 5.66675 3.33325C5.11446 3.33325 4.66675 3.78097 4.66675 4.33325C4.66675 4.88554 5.11446 5.33325 5.66675 5.33325Z" fill="#596168"/>
6
+ <path d="M10.3335 13.3333C10.8858 13.3333 11.3335 12.8855 11.3335 12.3333C11.3335 11.781 10.8858 11.3333 10.3335 11.3333C9.78121 11.3333 9.3335 11.781 9.3335 12.3333C9.3335 12.8855 9.78121 13.3333 10.3335 13.3333Z" fill="#596168"/>
7
+ <path d="M5.66675 13.3333C6.21903 13.3333 6.66675 12.8855 6.66675 12.3333C6.66675 11.781 6.21903 11.3333 5.66675 11.3333C5.11446 11.3333 4.66675 11.781 4.66675 12.3333C4.66675 12.8855 5.11446 13.3333 5.66675 13.3333Z" fill="#596168"/>
8
+ </svg>
@@ -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 {
@@ -239,4 +240,6 @@ export const RepositoryAction = {
239
240
  CONTAINER: "CONTAINER",
240
241
  CHART_PULL: "CHART_PULL",
241
242
  CHART_PUSH: "CHART_PUSH"
242
- }
243
+ }
244
+
245
+ export const MAX_Z_INDEX = 2147483647
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import { ReactComponent as ICDrag } from '../../Assets/Icon/ic-drag.svg'
3
+ import { DraggableButtonProps } from './types'
4
+
5
+ export default function DraggableButton({ dragClassName }: DraggableButtonProps) {
6
+ return (
7
+ <button type="button" className="dc__outline-none-imp dc__no-border p-0 dc__transparent h-20">
8
+ <ICDrag className={`${dragClassName} dc__grabbable icon-dim-20 fcn-6`} />
9
+ </button>
10
+ )
11
+ }
@@ -0,0 +1,109 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import Draggable, { ControlPosition, DraggableData } from 'react-draggable'
3
+ import { DraggableWrapperProps, DraggablePositionVariant } from './types'
4
+ import { useWindowSize } from '../Hooks'
5
+ import { MAX_Z_INDEX } from '../Constants'
6
+
7
+ /**
8
+ * TODO: import it as lazy, after it is supported in common
9
+ * 1. If using react select please use menuPlacement='auto'
10
+ * 2. dragClassName will be used to identify the grabbable button that will grab the div to drag
11
+ * 3. parentRef is the reference point from which we will derive the base top:0 ,left: 0 position
12
+ */
13
+ export default function DraggableWrapper({
14
+ children,
15
+ zIndex = MAX_Z_INDEX,
16
+ positionVariant,
17
+ dragClassName,
18
+ parentRef,
19
+ boundaryGap = 16,
20
+ childDivProps = {},
21
+ }: DraggableWrapperProps) {
22
+ const windowSize = useWindowSize()
23
+ const nodeRef = useRef<HTMLDivElement>(null)
24
+
25
+ const getDefaultPosition = (positionVariant: DraggablePositionVariant): ControlPosition => {
26
+ // if i return x: 0, y: 0 then it will be top left corner of parentDiv
27
+ const parentRect = parentRef?.current?.getBoundingClientRect() ?? {
28
+ width: 0,
29
+ height: 0,
30
+ top: 0,
31
+ left: 0,
32
+ bottom: 0,
33
+ }
34
+
35
+ switch (positionVariant) {
36
+ case DraggablePositionVariant.PARENT_BOTTOM_CENTER: {
37
+ const x = parentRect.width / 2 - nodeRef.current?.getBoundingClientRect().width / 2
38
+ const y =
39
+ Math.min(parentRect.height, windowSize.height) -
40
+ parentRect.top -
41
+ nodeRef.current?.getBoundingClientRect().height -
42
+ boundaryGap
43
+ return { x, y }
44
+ }
45
+ case DraggablePositionVariant.SCREEN_BOTTOM_CENTER: {
46
+ const x = windowSize.width / 2 - parentRect.left - nodeRef.current?.getBoundingClientRect().width / 2
47
+ const y =
48
+ windowSize.height - parentRect.top - nodeRef.current?.getBoundingClientRect().height - boundaryGap
49
+
50
+ return { x, y }
51
+ }
52
+ // Add more cases for other variants if needed
53
+ default:
54
+ throw new Error('Please use DraggablePositionVariant enum')
55
+ }
56
+ }
57
+
58
+ const [position, setPosition] = useState<ControlPosition>({
59
+ x: 0,
60
+ y: 0,
61
+ })
62
+
63
+ // On change of windowSize we will reset the position to default
64
+ useEffect(() => {
65
+ const defaultPosition = getDefaultPosition(positionVariant)
66
+ setPosition(defaultPosition)
67
+ }, [nodeRef, positionVariant, windowSize])
68
+
69
+ function handlePositionChange(e, data: DraggableData) {
70
+ const offsetX = parentRef?.current?.getBoundingClientRect().left ?? 0
71
+ const offsetY = parentRef?.current?.getBoundingClientRect().top ?? 0
72
+
73
+ if (
74
+ offsetX + data.x + nodeRef.current?.getBoundingClientRect().width + boundaryGap > windowSize.width ||
75
+ offsetY + data.y + nodeRef.current?.getBoundingClientRect().height + boundaryGap > windowSize.height ||
76
+ offsetX + data.x < 0 ||
77
+ offsetY + data.y < 0
78
+ ) {
79
+ return
80
+ }
81
+
82
+ setPosition({
83
+ x: data.x,
84
+ y: data.y,
85
+ })
86
+ }
87
+
88
+ return (
89
+ // Check there might be a case if we need it to be absolute?
90
+ <div
91
+ className="dc__position-fixed"
92
+ style={{
93
+ zIndex,
94
+ }}
95
+ >
96
+ <Draggable handle={`.${dragClassName}`} nodeRef={nodeRef} position={position} onDrag={handlePositionChange}>
97
+ <div
98
+ ref={nodeRef}
99
+ {...childDivProps}
100
+ style={{
101
+ zIndex,
102
+ }}
103
+ >
104
+ {children}
105
+ </div>
106
+ </Draggable>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,3 @@
1
+ export { default as DraggableWrapper } from './DraggableWrapper'
2
+ export { default as DraggableButton } from './DraggableButton'
3
+ export * from './types'
@@ -0,0 +1,22 @@
1
+ import { HTMLAttributes } from 'react'
2
+
3
+ export enum DraggablePositionVariant {
4
+ PARENT_BOTTOM_CENTER = 'PARENT_BOTTOM_CENTER',
5
+ SCREEN_BOTTOM_CENTER = 'SCREEN_BOTTOM_CENTER',
6
+ // Can add more based on requirement
7
+ }
8
+
9
+ export interface DraggableWrapperProps {
10
+ children: React.ReactNode
11
+ zIndex?: number
12
+ positionVariant?: DraggablePositionVariant
13
+ dragClassName: string
14
+ parentRef?: React.RefObject<HTMLDivElement>
15
+ boundaryGap?: number
16
+ childDivProps?: HTMLAttributes<HTMLDivElement>
17
+ }
18
+
19
+ /**
20
+ * dragClassName is the class that we feed to Draggable to identify dragging buttons
21
+ */
22
+ export interface DraggableButtonProps extends Pick<DraggableWrapperProps, 'dragClassName'> {}
@@ -10,6 +10,7 @@ export default function Grid({
10
10
  containerClass = '',
11
11
  itemClass = '',
12
12
  children,
13
+ ...containerDivProps
13
14
  }: GridProps) {
14
15
  const containerStyles = container ? { gap: spacing + 'px' } : {}
15
16
 
@@ -25,14 +26,18 @@ export default function Grid({
25
26
  }
26
27
 
27
28
  return (
28
- <div className={`p-0 ${itemClass}`} style={itemStyles}>
29
+ <div className={`p-0 ${itemClass}`} style={itemStyles} {...containerDivProps}>
29
30
  {children}
30
31
  </div>
31
32
  )
32
33
  }
33
34
 
34
35
  return (
35
- <div className={`flex-wrap flexbox ${container ? containerClass : ''}`} style={containerStyles}>
36
+ <div
37
+ className={`flex-wrap flexbox ${container ? containerClass : ''}`}
38
+ style={containerStyles}
39
+ {...containerDivProps}
40
+ >
36
41
  {children}
37
42
  </div>
38
43
  )
@@ -1,4 +1,6 @@
1
- export interface GridProps {
1
+ import { HTMLAttributes } from "react"
2
+
3
+ export interface GridProps extends HTMLAttributes<HTMLDivElement> {
2
4
  container?: boolean
3
5
  spacing?: number
4
6
  item?: boolean
@@ -7,6 +7,8 @@ import { ERROR_EMPTY_SCREEN, TOKEN_COOKIE_NAME } from './Constants'
7
7
  import { ServerErrors } from './ServerError'
8
8
  import { toastAccessDenied } from './ToastBody'
9
9
  import { AsyncOptions, AsyncState, UseSearchString } from './Types'
10
+ import { JSONPath } from 'jsonpath-plus'
11
+ import * as jsonpatch from 'fast-json-patch'
10
12
 
11
13
  toast.configure({
12
14
  autoClose: 3000,
@@ -458,3 +460,102 @@ export const getUrlWithSearchParams = (url: string, params: Record<string | numb
458
460
  * Custom exception logger function for logging errors to sentry
459
461
  */
460
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])) {
506
+ if (obj[key].length === 0) continue
507
+ obj[key] = obj[key].filter((item) => item !== null)
508
+ // Check if the array is empty
509
+ if (obj[key].length === 0) {
510
+ delete obj[key] // Delete the key if the array is empty
511
+ }
512
+ } else if (obj[key] && typeof obj[key] === 'object') {
513
+ if (removeEmptyObjectKeysAndNullValues(obj[key])) {
514
+ delete obj[key]
515
+ }
516
+ } else if (obj[key] === undefined) {
517
+ delete obj[key]
518
+ }
519
+ }
520
+ return Object.keys(obj).length === 0
521
+ }
522
+
523
+ export function getUnlockedJSON(json, jsonPathArray) {
524
+ const jsonCopy = JSON.parse(JSON.stringify(json))
525
+ let patches = jsonPathArray.flatMap((jsonPath) => {
526
+ const pathsToRemove = JSONPath({ path: jsonPath, json: jsonCopy, resultType: 'all' })
527
+ return pathsToRemove.map((result) =>
528
+ Array.isArray(result.parent)
529
+ ? { op: 'replace', path: result.pointer, value: null }
530
+ : { op: 'remove', path: result.pointer },
531
+ )
532
+ })
533
+ let newDocument = jsonpatch.applyPatch(jsonCopy, patches).newDocument
534
+
535
+ removeEmptyObjectKeysAndNullValues(newDocument)
536
+ return newDocument
537
+ }
538
+
539
+ export function getLockedJSON(json, jsonPathArray: string[]) {
540
+ const jsonCopy = JSON.parse(JSON.stringify(json))
541
+ let resultJson = {}
542
+ jsonPathArray.forEach((jsonPath) => {
543
+ const elements = JSONPath({ path: jsonPath, json: jsonCopy, resultType: 'all' })
544
+ elements.forEach((element) => {
545
+ const pathArray: string[] = JSONPath.toPathArray(element.path)
546
+ const lastPath = pathArray.pop()
547
+ let current = resultJson
548
+ for (let i = 0; i < pathArray.length; i++) {
549
+ let key = isNaN(Number(pathArray[i])) ? pathArray[i] : parseInt(pathArray[i])
550
+ if (!current[key]) {
551
+ current[key] = isNaN(Number(pathArray[i + 1]??lastPath)) ? {} : []
552
+ }
553
+ current = current[key]
554
+ }
555
+ let key = isNaN(Number(lastPath)) ? lastPath : parseInt(lastPath)
556
+ current[key] = element.value
557
+ })
558
+ })
559
+ return resultJson['$']
560
+ }
561
+
@@ -0,0 +1,30 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ function getSize() {
4
+ return {
5
+ width: window.innerWidth,
6
+ height: window.innerHeight,
7
+ }
8
+ }
9
+
10
+ export function useWindowSize() {
11
+ let timeout
12
+ const [windowSize, setWindowSize] = useState(getSize)
13
+
14
+ function handleResize(e) {
15
+ if (timeout) {
16
+ window.cancelAnimationFrame(timeout)
17
+ }
18
+
19
+ timeout = window.requestAnimationFrame(function () {
20
+ setWindowSize(getSize())
21
+ })
22
+ }
23
+
24
+ useEffect(() => {
25
+ window.addEventListener('resize', handleResize, false)
26
+ return () => window.removeEventListener('resize', handleResize, false)
27
+ }, [])
28
+
29
+ return windowSize
30
+ }
@@ -1,2 +1,3 @@
1
1
  export { useSuperAdmin } from './UseSuperAdmin/UseSuperAdmin'
2
2
  export { useClickOutside } from './UseClickOutside/UseClickOutside'
3
+ export { useWindowSize } from './UseWindowSize/UseWindowSize'
@@ -631,6 +631,75 @@ export interface CiPipeline {
631
631
  isOffendingMandatoryPlugin?: boolean
632
632
  pipelineType?: string
633
633
  }
634
+ export interface InformationBarProps {
635
+ text: string
636
+ className?: string
637
+ children?: React.ReactNode
638
+ }
639
+
640
+ export interface CodeEditorInterface {
641
+ value?: string
642
+ lineDecorationsWidth?: number
643
+ responseType?: string
644
+ onChange?: (string) => void
645
+ onBlur?: () => void
646
+ onFocus?: () => void
647
+ children?: any
648
+ defaultValue?: string
649
+ mode?: 'json' | 'yaml' | 'shell' | 'dockerfile' | 'plaintext'
650
+ tabSize?: number
651
+ readOnly?: boolean
652
+ noParsing?: boolean
653
+ minHeight?: number
654
+ maxHeight?: number
655
+ inline?: boolean
656
+ height?: number | string
657
+ shebang?: string | JSX.Element
658
+ diffView?: boolean
659
+ loading?: boolean
660
+ customLoader?: JSX.Element
661
+ theme?: string
662
+ original?: string
663
+ focus?: boolean
664
+ validatorSchema?: any
665
+ isKubernetes?: boolean
666
+ cleanData?: boolean
667
+ chartVersion?: any
668
+ }
669
+
670
+ export interface CodeEditorComposition {
671
+ Header?: React.FC<any>
672
+ LanguageChanger?: React.FC<any>
673
+ ThemeChanger?: React.FC<any>
674
+ ValidationError?: React.FC<any>
675
+ Clipboard?: React.FC<any>
676
+ Warning?: React.FC<InformationBarProps>
677
+ ErrorBar?: React.FC<InformationBarProps>
678
+ Information?: React.FC<InformationBarProps>
679
+ }
680
+
681
+ export interface CodeEditorHeaderComposition {
682
+ LanguageChanger?: React.FC<any>;
683
+ ThemeChanger?: React.FC<any>;
684
+ ValidationError?: React.FC<any>;
685
+ Clipboard?: React.FC<any>;
686
+ }
687
+ export interface CodeEditorHeaderInterface {
688
+ children?: any;
689
+ className?: string
690
+ hideDefaultSplitHeader?: boolean;
691
+ }
692
+
693
+ export interface DeploymentChartVersionType {
694
+ chartRefId: number
695
+ chartVersion: string
696
+ chartType: string
697
+ type: number
698
+ }
699
+
700
+ export interface ChartVersionAndTypeSelectorProps {
701
+ setSelectedChartRefId: React.Dispatch<React.SetStateAction<number>>
702
+ }
634
703
 
635
704
  export enum PipelineType {
636
705
  CI_PIPELINE = 'CI_PIPELINE',
@@ -42,5 +42,7 @@ 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'
45
46
  export * from './AddCDButton'
46
47
  export * from './CustomInput'
48
+ export * from './DraggableWrapper'