From e640804c04337d2f4ce49c875f005407ce14de3c Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Thu, 17 Feb 2022 22:05:18 +0200 Subject: [PATCH 1/9] feat: create base prev conf editor layout --- .../PreviewConfigurationEditor.tsx | 20 ++++++++++++++++- .../PreviewConfigurationEditor/styled.tsx | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/components/organisms/PreviewConfigurationEditor/styled.tsx diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx index 403d3614a6..82ccdd23ff 100644 --- a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -1,5 +1,23 @@ +import {Input} from 'antd'; + +import * as S from './styled'; + const PreviewConfigurationEditor = () => { - return
; + return ( +
+ + Name your configuration: + + + + Select which values files to use: + Drag and drop to specify order + + + Specify options: + +
+ ); }; export default PreviewConfigurationEditor; diff --git a/src/components/organisms/PreviewConfigurationEditor/styled.tsx b/src/components/organisms/PreviewConfigurationEditor/styled.tsx new file mode 100644 index 0000000000..68a7db3edb --- /dev/null +++ b/src/components/organisms/PreviewConfigurationEditor/styled.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +import Colors from '@styles/Colors'; + +export const Label = styled.p` + margin: 0; + font-size: 14px; + font-weight: 600; + color: ${Colors.grey9}; + margin-bottom: 8px; +`; + +export const Field = styled.div` + margin-top: 6px; + margin-bottom: 30px; +`; + +export const Description = styled.p` + margin: 0; + font-size: 14px; + color: ${Colors.grey7}; +`; From 0c3163ceaed4d44c2dad7e819135143afcfec180 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 11:48:56 +0200 Subject: [PATCH 2/9] feat: helm install/template options constants --- src/constants/helmOptions.ts | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/constants/helmOptions.ts diff --git a/src/constants/helmOptions.ts b/src/constants/helmOptions.ts new file mode 100644 index 0000000000..9b7e88ddf9 --- /dev/null +++ b/src/constants/helmOptions.ts @@ -0,0 +1,53 @@ +const helmCommonOptions = { + '--atomic': 'boolean', + '--ca-file': 'string', + '--create-namespace': 'boolean', + '--dependency-update': 'boolean', + '--description': 'string', + '--devel': 'boolean', + '--disable-openapi-validation': 'boolean', + '--dry-run': 'boolean', + '--generate-name': 'boolean', + '--help': 'boolean', + '--insecure-skip-tls-verify': 'boolean', + '--key-file': 'string', + '--keyring': 'string', + '--name-template': 'string', + '--no-hooks': 'boolean', + '--pass-credentials': 'boolean', + '--password': 'string', + '--post-renderer': 'string', + '--render-subchart-notes': 'boolean', + '--replace': 'boolean', + '--repo': 'string', + '--set': 'stringArray', + '--set-file': 'stringArray', + '--set-string': 'stringArray', + '--skip-crds': 'boolean', + '--timeout': 'duration', + '--username': 'string', + '--values': 'strings', + '--verify': 'boolean', + '--version': 'string', + '--wait': 'boolean', + '--wait-for-jobs': 'boolean', +}; + +export const helmInstallOptions = { + ...helmCommonOptions, + '--output': 'string', // ['table', 'json', 'yaml'], +}; + +export const helmTemplateOptions = { + ...helmCommonOptions, + '--api-versions': 'stringArray', + '--cert-file': 'string', + '--include-crds': 'boolean', + '--is-upgrade': 'boolean', + '--kube-version': 'string', + '--output-dir': 'string', + '--release-name': 'boolean', + '--show-only': 'stringArray', + '--skip-tests': 'boolean', + '--validate': 'boolean', +}; From 5fe74cb947012d7ea48ac9e17b8b269c81c9d085 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 13:00:24 +0200 Subject: [PATCH 3/9] refactor: KeyValueInput to accept schema and different value types --- .../KeyValueInput/KeyValueEntryRenderer.tsx | 93 +++++++++++ .../atoms/KeyValueInput/KeyValueInput.tsx | 158 ++++++------------ .../atoms/KeyValueInput/constants.ts | 1 + src/components/atoms/KeyValueInput/styled.tsx | 34 ++++ src/components/atoms/KeyValueInput/types.ts | 2 + .../ResourceFilter/ResourceFilter.tsx | 13 +- 6 files changed, 193 insertions(+), 108 deletions(-) create mode 100644 src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx create mode 100644 src/components/atoms/KeyValueInput/constants.ts create mode 100644 src/components/atoms/KeyValueInput/styled.tsx create mode 100644 src/components/atoms/KeyValueInput/types.ts diff --git a/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx new file mode 100644 index 0000000000..83c44169ca --- /dev/null +++ b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx @@ -0,0 +1,93 @@ +import React from 'react'; + +import {Input, Select} from 'antd'; + +import {MinusOutlined} from '@ant-design/icons'; + +import Colors from '@styles/Colors'; + +import {ANY_VALUE} from './constants'; +import {KeyValueEntry} from './types'; + +import * as S from './styled'; + +type KeyValueEntryRendererProps = { + entry: KeyValueEntry; + valueType?: string; + onKeyChange: (newKey: string) => void; + onValueChange: (newValue: string) => void; + onEntryRemove: (entryId: string) => void; + disabled?: boolean; + availableKeys: string[]; + availableValues?: string[]; +}; + +type ValueInputProps = { + value?: string; + valueType: string; + availableValues?: string[]; + onChange: (newValue: string) => void; + disabled?: boolean; +}; + +const ValueInput: React.FC = props => { + const {value, valueType, availableValues, disabled, onChange} = props; + + if (valueType === 'string') { + if (availableValues?.length) { + return ( + + ); + } + return onChange(e.target.value)} disabled={disabled} />; + } + + // TODO: decide if we want to implement more value types + return null; +}; + +const KeyValueEntryRenderer: React.FC = props => { + const {entry, valueType, onKeyChange, onValueChange, onEntryRemove, disabled, availableKeys, availableValues} = props; + + return ( + + + + + {entry.key && valueType && valueType !== 'boolean' && ( + + )} + + + onEntryRemove(entry.id)} + color={Colors.redError} + size="small" + icon={} + /> + + ); +}; + +export default KeyValueEntryRenderer; diff --git a/src/components/atoms/KeyValueInput/KeyValueInput.tsx b/src/components/atoms/KeyValueInput/KeyValueInput.tsx index a6cc6d73f5..75f7d6ef70 100644 --- a/src/components/atoms/KeyValueInput/KeyValueInput.tsx +++ b/src/components/atoms/KeyValueInput/KeyValueInput.tsx @@ -1,62 +1,30 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; -import {Button, Select} from 'antd'; +import {Button} from 'antd'; -import {MinusOutlined, PlusOutlined} from '@ant-design/icons'; +import {PlusOutlined} from '@ant-design/icons'; import isDeepEqual from 'fast-deep-equal/es6/react'; -import styled from 'styled-components'; import {v4 as uuidv4} from 'uuid'; -import Colors from '@styles/Colors'; - -const Container = styled.div` - max-height: 800px; - overflow-y: auto; -`; - -const TitleContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; -`; -const TitleLabel = styled.span``; - -const KeyValueContainer = styled.div` - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - grid-gap: 8px; - align-items: center; - margin: 10px 0; -`; - -const KeyValueRemoveButtonContainer = styled.div` - display: grid; - grid-template-columns: 1fr max-content; - grid-gap: 8px; - align-items: center; -`; - -const StyledRemoveButton = styled(Button)` - min-width: 24px; -`; - -type KeyValueEntry = {id: string; key?: string; value?: string}; -type KeyValue = Record; +import KeyValueEntryRenderer from './KeyValueEntryRenderer'; +import {ANY_VALUE} from './constants'; +import {KeyValueData, KeyValueEntry} from './types'; + +import * as S from './styled'; type KeyValueInputProps = { disabled?: boolean; label: string; labelStyle?: React.CSSProperties; + schema: Record; data: Record; - value: KeyValue; - onChange: (keyValues: KeyValue) => void; + value: KeyValueData; + onChange: (keyValueData: KeyValueData) => void; }; -export const ANY_VALUE = ''; - -function makeKeyValueFromEntries(keyValueEntries: KeyValueEntry[]): KeyValue { - const keyValue: KeyValue = {}; +function makeKeyValueDataFromEntries(keyValueEntries: KeyValueEntry[]): KeyValueData { + const keyValue: KeyValueData = {}; keyValueEntries.forEach(({key, value}) => { if (!key || !value) { return; @@ -71,15 +39,15 @@ function makeKeyValueFromEntries(keyValueEntries: KeyValueEntry[]): KeyValue { } function KeyValueInput(props: KeyValueInputProps) { - const {disabled = false, label, labelStyle, data, value: keyValue, onChange} = props; + const {disabled = false, label, labelStyle, data, value: parentKeyValueData, schema, onChange} = props; const [entries, setEntries] = useState([]); - const [currentKeyValue, setCurrentKeyValue] = useState(keyValue); + const [currentKeyValueData, setCurrentKeyValueData] = useState(parentKeyValueData); useEffect(() => { - if (!isDeepEqual(keyValue, currentKeyValue)) { - setCurrentKeyValue(keyValue); + if (!isDeepEqual(parentKeyValueData, currentKeyValueData)) { + setCurrentKeyValueData(parentKeyValueData); const newEntries: KeyValueEntry[] = []; - Object.entries(keyValue).forEach(([key, value]) => { + Object.entries(parentKeyValueData).forEach(([key, value]) => { if (newEntries.some(e => e.key === key)) { return; } @@ -100,12 +68,12 @@ function KeyValueInput(props: KeyValueInputProps) { }); setEntries(newEntries); } - }, [keyValue, currentKeyValue, data]); + }, [parentKeyValueData, currentKeyValueData, data]); // do we need "data" as dep? const updateKeyValue = (newEntries: KeyValueEntry[]) => { - const newKeyValue = makeKeyValueFromEntries(newEntries); - setCurrentKeyValue(newKeyValue); - onChange(newKeyValue); + const newKeyValueData = makeKeyValueDataFromEntries(newEntries); + setCurrentKeyValueData(newKeyValueData); + onChange(newKeyValueData); }; const createEntry = () => { @@ -145,62 +113,44 @@ function KeyValueInput(props: KeyValueInputProps) { updateKeyValue(newEntries); }; + const getEntryAvailableKeys = useCallback( + (entry: KeyValueEntry) => { + return Object.keys(schema).filter(key => key === entry.key || !entries.some(e => e.key === key)); + }, + [schema, entries] + ); + + const getEntryAvailableValues = useCallback( + (entry: KeyValueEntry) => { + if (entry.key && data[entry.key]) { + return data[entry.key]; + } + return undefined; + }, + [data] + ); + return ( - - - {label} + + + {label} - + {entries.map(entry => ( - - - - - {entry.key && ( - - )} - - - removeEntry(entry.id)} - color={Colors.redError} - size="small" - icon={} - /> - + updateEntryKey(entry.id, newKey)} + onValueChange={newValue => updateEntryValue(entry.id, newValue)} + onEntryRemove={removeEntry} + availableKeys={getEntryAvailableKeys(entry)} + availableValues={getEntryAvailableValues(entry)} + /> ))} - + ); } diff --git a/src/components/atoms/KeyValueInput/constants.ts b/src/components/atoms/KeyValueInput/constants.ts new file mode 100644 index 0000000000..e6edb94c08 --- /dev/null +++ b/src/components/atoms/KeyValueInput/constants.ts @@ -0,0 +1 @@ +export const ANY_VALUE = ''; diff --git a/src/components/atoms/KeyValueInput/styled.tsx b/src/components/atoms/KeyValueInput/styled.tsx new file mode 100644 index 0000000000..df52b620e8 --- /dev/null +++ b/src/components/atoms/KeyValueInput/styled.tsx @@ -0,0 +1,34 @@ +import {Button} from 'antd'; + +import styled from 'styled-components'; + +export const Container = styled.div` + max-height: 800px; + overflow-y: auto; +`; + +export const TitleContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; +export const TitleLabel = styled.span``; + +export const KeyValueContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-gap: 8px; + align-items: center; + margin: 10px 0; +`; + +export const KeyValueRemoveButtonContainer = styled.div` + display: grid; + grid-template-columns: 1fr max-content; + grid-gap: 8px; + align-items: center; +`; + +export const StyledRemoveButton = styled(Button)` + min-width: 24px; +`; diff --git a/src/components/atoms/KeyValueInput/types.ts b/src/components/atoms/KeyValueInput/types.ts new file mode 100644 index 0000000000..77de45f4f5 --- /dev/null +++ b/src/components/atoms/KeyValueInput/types.ts @@ -0,0 +1,2 @@ +export type KeyValueEntry = {id: string; key?: string; value?: string}; +export type KeyValueData = Record; diff --git a/src/components/molecules/ResourceFilter/ResourceFilter.tsx b/src/components/molecules/ResourceFilter/ResourceFilter.tsx index 224d12f5f9..9c45369491 100644 --- a/src/components/molecules/ResourceFilter/ResourceFilter.tsx +++ b/src/components/molecules/ResourceFilter/ResourceFilter.tsx @@ -3,6 +3,7 @@ import {useDebounce} from 'react-use'; import {Button, Input, Select} from 'antd'; +import {mapValues} from 'lodash'; import styled from 'styled-components'; import {DEFAULT_EDITOR_DEBOUNCE} from '@constants/constants'; @@ -104,13 +105,15 @@ const ResourceFilter = () => { ].sort(); }, [knownResourceKinds, resourceMap]); - const allLabels = useMemo>(() => { + const allLabelsData = useMemo>(() => { return makeKeyValuesFromObjectList(Object.values(resourceMap), resource => resource.content?.metadata?.labels); }, [resourceMap]); + const allLabelsSchema = useMemo(() => mapValues(allLabelsData, () => 'string'), [allLabelsData]); - const allAnnotations = useMemo>(() => { + const allAnnotationsData = useMemo>(() => { return makeKeyValuesFromObjectList(Object.values(resourceMap), resource => resource.content?.metadata?.annotations); }, [resourceMap]); + const allAnnotationsSchema = useMemo(() => mapValues(allAnnotationsData, () => 'string'), [allAnnotationsData]); const fileOrFolderContainedInOptions = useMemo(() => { return Object.keys(fileMap).map(option => ( @@ -275,7 +278,8 @@ const ResourceFilter = () => { { From fa65358f0780f3bb989195b1e8c96652f47d5ce7 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 13:23:35 +0200 Subject: [PATCH 4/9] refactor: KeyValueInput improvements --- .../KeyValueInput/KeyValueEntryRenderer.tsx | 37 +----------------- .../atoms/KeyValueInput/KeyValueInput.tsx | 9 ++++- .../atoms/KeyValueInput/ValueInput.tsx | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 src/components/atoms/KeyValueInput/ValueInput.tsx diff --git a/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx index 83c44169ca..f81466386c 100644 --- a/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx +++ b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import {Input, Select} from 'antd'; +import {Select} from 'antd'; import {MinusOutlined} from '@ant-design/icons'; import Colors from '@styles/Colors'; -import {ANY_VALUE} from './constants'; +import ValueInput from './ValueInput'; import {KeyValueEntry} from './types'; import * as S from './styled'; @@ -22,39 +22,6 @@ type KeyValueEntryRendererProps = { availableValues?: string[]; }; -type ValueInputProps = { - value?: string; - valueType: string; - availableValues?: string[]; - onChange: (newValue: string) => void; - disabled?: boolean; -}; - -const ValueInput: React.FC = props => { - const {value, valueType, availableValues, disabled, onChange} = props; - - if (valueType === 'string') { - if (availableValues?.length) { - return ( - - ); - } - return onChange(e.target.value)} disabled={disabled} />; - } - - // TODO: decide if we want to implement more value types - return null; -}; - const KeyValueEntryRenderer: React.FC = props => { const {entry, valueType, onKeyChange, onValueChange, onEntryRemove, disabled, availableKeys, availableValues} = props; diff --git a/src/components/atoms/KeyValueInput/KeyValueInput.tsx b/src/components/atoms/KeyValueInput/KeyValueInput.tsx index 75f7d6ef70..d94e7e5343 100644 --- a/src/components/atoms/KeyValueInput/KeyValueInput.tsx +++ b/src/components/atoms/KeyValueInput/KeyValueInput.tsx @@ -52,11 +52,13 @@ function KeyValueInput(props: KeyValueInputProps) { return; } + const availableValues: string[] | undefined = data[key]; + if (value === null) { newEntries.push({ id: uuidv4(), key, - value: ANY_VALUE, + value: availableValues?.length ? ANY_VALUE : undefined, }); } else { newEntries.push({ @@ -93,10 +95,13 @@ function KeyValueInput(props: KeyValueInputProps) { const updateEntryKey = (entryId: string, key: string) => { const newEntries = Array.from(entries); const entryIndex = newEntries.findIndex(e => e.id === entryId); + + const availableValues: string[] | undefined = data[key]; + newEntries[entryIndex] = { id: entryId, key, - value: ANY_VALUE, + value: availableValues?.length ? ANY_VALUE : undefined, }; setEntries(newEntries); updateKeyValue(newEntries); diff --git a/src/components/atoms/KeyValueInput/ValueInput.tsx b/src/components/atoms/KeyValueInput/ValueInput.tsx new file mode 100644 index 0000000000..62982b84f7 --- /dev/null +++ b/src/components/atoms/KeyValueInput/ValueInput.tsx @@ -0,0 +1,39 @@ +import {Input, Select} from 'antd'; + +import {ANY_VALUE} from './constants'; + +type ValueInputProps = { + value?: string; + valueType: string; + availableValues?: string[]; + onChange: (newValue: string) => void; + disabled?: boolean; +}; + +const ValueInput: React.FC = props => { + const {value, valueType, availableValues, disabled, onChange} = props; + + // TODO: decide if we need a custom input for the stringArray value type + if (valueType === 'string' || valueType === 'stringArray') { + if (availableValues?.length) { + return ( + + ); + } + return onChange(e.target.value)} disabled={disabled} />; + } + + // TODO: decide if we want to implement more value types + return null; +}; + +export default ValueInput; From fe427a91b74c7435bb6cddf3ff2a4e64f4f1fea5 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 13:25:10 +0200 Subject: [PATCH 5/9] feat: add KeyValueInput for preview conf opts --- .../PreviewConfigurationEditor.tsx | 34 +++++++++++++++++-- src/constants/helmOptions.ts | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx index 82ccdd23ff..5a5451b657 100644 --- a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -1,8 +1,25 @@ -import {Input} from 'antd'; +import {useState} from 'react'; + +import {Input, Select} from 'antd'; + +import {helmInstallOptions, helmTemplateOptions} from '@constants/helmOptions'; + +import {useAppSelector} from '@redux/hooks'; + +import {KeyValueInput} from '@components/atoms'; import * as S from './styled'; const PreviewConfigurationEditor = () => { + const helmPreviewMode = useAppSelector( + state => state.config.projectConfig?.settings?.helmPreviewMode || state.config.settings.helmPreviewMode + ); + + const [helmOptions, setHelmOptions] = useState({}); + const [helmCommand, setHelmCommand] = useState<'template' | 'install'>(helmPreviewMode || 'template'); + + const keyValueInputSchema = helmCommand === 'template' ? helmTemplateOptions : helmInstallOptions; + return (
@@ -14,7 +31,20 @@ const PreviewConfigurationEditor = () => { Drag and drop to specify order - Specify options: + Select which helm command to use for this Preview: + + + +
); diff --git a/src/constants/helmOptions.ts b/src/constants/helmOptions.ts index 9b7e88ddf9..cd8698d875 100644 --- a/src/constants/helmOptions.ts +++ b/src/constants/helmOptions.ts @@ -26,7 +26,7 @@ const helmCommonOptions = { '--skip-crds': 'boolean', '--timeout': 'duration', '--username': 'string', - '--values': 'strings', + '--values': 'string', '--verify': 'boolean', '--version': 'string', '--wait': 'boolean', From 4958395edf9d217981cfd089fba278aa33c05e38 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 15:43:28 +0200 Subject: [PATCH 6/9] feat: OrderedList atom component --- src/App.tsx | 10 +-- .../atoms/OrderedList/OrderedList.tsx | 66 +++++++++++++++++++ src/components/atoms/OrderedList/index.ts | 2 + src/components/atoms/OrderedList/styled.tsx | 12 ++++ src/components/atoms/index.ts | 1 + .../PreviewConfigurationEditor.tsx | 28 +++++++- src/models/appstate.ts | 4 ++ src/models/ui.ts | 1 - .../PreviewConfigurationQuickAction.tsx | 4 +- src/redux/initialState.ts | 4 +- src/redux/reducers/main.ts | 15 +++++ src/redux/reducers/ui.ts | 8 --- src/utils/array.ts | 23 +++++++ 13 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 src/components/atoms/OrderedList/OrderedList.tsx create mode 100644 src/components/atoms/OrderedList/index.ts create mode 100644 src/components/atoms/OrderedList/styled.tsx create mode 100644 src/utils/array.ts diff --git a/src/App.tsx b/src/App.tsx index 40cf71f7b7..f21e79c3ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,12 +20,8 @@ import {useAppSelector} from '@redux/hooks'; import {setAlert} from '@redux/reducers/alert'; import {setCreateProject, setLoadingProject, setOpenProject} from '@redux/reducers/appConfig'; import {closePluginsDrawer} from '@redux/reducers/extension'; -import { - closeFolderExplorer, - closePreviewConfigurationEditor, - toggleNotifications, - toggleSettings, -} from '@redux/reducers/ui'; +import {closePreviewConfigurationEditor} from '@redux/reducers/main'; +import {closeFolderExplorer, toggleNotifications, toggleSettings} from '@redux/reducers/ui'; import {isInClusterModeSelector, kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; import {loadContexts} from '@redux/thunks/loadKubeConfig'; @@ -78,7 +74,7 @@ const App = () => { const dispatch = useDispatch(); const isChangeFiltersConfirmModalVisible = useAppSelector(state => state.main.filtersToBeChanged); const isClusterDiffModalVisible = useAppSelector(state => state.ui.isClusterDiffVisible); - const isPreviewConfigurationEditorOpen = useAppSelector(state => state.ui.isPreviewConfigurationEditorOpen); + const isPreviewConfigurationEditorOpen = useAppSelector(state => state.main.prevConfEditor.isOpen); const isClusterSelectorVisible = useAppSelector(state => state.config.isClusterSelectorVisible); const isCreateFolderModalVisible = useAppSelector(state => state.ui.createFolderModal.isOpen); const isCreateProjectModalVisible = useAppSelector(state => state.ui.createProjectModal.isOpen); diff --git a/src/components/atoms/OrderedList/OrderedList.tsx b/src/components/atoms/OrderedList/OrderedList.tsx new file mode 100644 index 0000000000..db06271ac8 --- /dev/null +++ b/src/components/atoms/OrderedList/OrderedList.tsx @@ -0,0 +1,66 @@ +import {useCallback} from 'react'; + +import {Checkbox} from 'antd'; + +import {ArrowDownOutlined, ArrowUpOutlined} from '@ant-design/icons'; + +import {arrayMove} from '@utils/array'; + +import * as S from './styled'; + +export type OrderedListItem = { + id: string; + text: string; + isChecked: boolean; +}; + +type OrderedListProps = { + items: OrderedListItem[]; + onChange: (items: OrderedListItem[]) => void; +}; + +const OrderedList: React.FC = props => { + const {items, onChange} = props; + + const checkItem = useCallback( + (itemId: string) => { + onChange(items.slice().map(item => (item.id === itemId ? {...item, isChecked: !item.isChecked} : item))); + }, + [items, onChange] + ); + + const moveItem = useCallback( + (itemId: string, direction: 'up' | 'down') => { + const coefficient = direction === 'up' ? -1 : 1; + const itemIndex = items.findIndex(i => i.id === itemId); + if (!itemIndex) { + return; + } + onChange(arrayMove(items, itemIndex, itemIndex + coefficient)); + }, + [items, onChange] + ); + + return ( + + {items.map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + {index + 1}. + checkItem(item.id)}> + + {item.text} + + + + moveItem(item.id, 'up')} /> + moveItem(item.id, 'down')} /> + + + ))} + + ); +}; + +export default OrderedList; diff --git a/src/components/atoms/OrderedList/index.ts b/src/components/atoms/OrderedList/index.ts new file mode 100644 index 0000000000..79b097ae5c --- /dev/null +++ b/src/components/atoms/OrderedList/index.ts @@ -0,0 +1,2 @@ +export {default} from './OrderedList'; +export type {OrderedListItem} from './OrderedList'; diff --git a/src/components/atoms/OrderedList/styled.tsx b/src/components/atoms/OrderedList/styled.tsx new file mode 100644 index 0000000000..0c3c0dae0c --- /dev/null +++ b/src/components/atoms/OrderedList/styled.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const List = styled.ol` + padding: 0; + margin-top: 8px; +`; + +export const ListItem = styled.li` + display: flex; + width: 100%; + justify-content: space-between; +`; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index a342a545d7..2eff6925a9 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -15,3 +15,4 @@ export {default as Dots} from './Dots'; export {default as Spinner} from './Spinner'; export {default as Icon} from './Icon'; export {default as TabHeader} from './TabHeader'; +export {default as OrderedList} from './OrderedList'; diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx index 5a5451b657..7e7725373f 100644 --- a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -4,9 +4,12 @@ import {Input, Select} from 'antd'; import {helmInstallOptions, helmTemplateOptions} from '@constants/helmOptions'; +import {HelmValuesFile} from '@models/helm'; + import {useAppSelector} from '@redux/hooks'; -import {KeyValueInput} from '@components/atoms'; +import {KeyValueInput, OrderedList} from '@components/atoms'; +import {OrderedListItem} from '@components/atoms/OrderedList'; import * as S from './styled'; @@ -15,11 +18,33 @@ const PreviewConfigurationEditor = () => { state => state.config.projectConfig?.settings?.helmPreviewMode || state.config.settings.helmPreviewMode ); + const helmChart = useAppSelector(state => { + const helmChartId = state.main.prevConfEditor.helmChartId; + if (!helmChartId) { + return undefined; + } + return state.main.helmChartMap[helmChartId]; + }); + + const valuesFiles = useAppSelector( + state => + helmChart?.valueFileIds + .map(id => state.main.helmValuesMap[id]) + .filter((v): v is HelmValuesFile => v !== undefined) || [] + ); + + const [valuesFileItems, setValuesFileItems] = useState( + valuesFiles.map(vf => ({id: vf.id, text: vf.name, isChecked: false})) + ); const [helmOptions, setHelmOptions] = useState({}); const [helmCommand, setHelmCommand] = useState<'template' | 'install'>(helmPreviewMode || 'template'); const keyValueInputSchema = helmCommand === 'template' ? helmTemplateOptions : helmInstallOptions; + if (!helmChart) { + return

Something went wrong, could not find the helm chart.

; + } + return (
@@ -29,6 +54,7 @@ const PreviewConfigurationEditor = () => { Select which values files to use: Drag and drop to specify order + Select which helm command to use for this Preview: diff --git a/src/models/appstate.ts b/src/models/appstate.ts index 7fc4a4164c..a2f55b9de0 100644 --- a/src/models/appstate.ts +++ b/src/models/appstate.ts @@ -146,6 +146,10 @@ interface AppState { /** type/value of filters that will be changed */ filtersToBeChanged?: ResourceFilterType; registeredKindHandlers: string[]; + prevConfEditor: { + isOpen: boolean; + helmChartId?: string; + }; } export type { diff --git a/src/models/ui.ts b/src/models/ui.ts index c7ff8104e0..10a77df9de 100644 --- a/src/models/ui.ts +++ b/src/models/ui.ts @@ -60,7 +60,6 @@ export type UiState = { isSettingsOpen: boolean; isClusterDiffVisible: boolean; isNotificationsOpen: boolean; - isPreviewConfigurationEditorOpen: boolean; newResourceWizard: { isOpen: boolean; defaultInput?: NewResourceWizardInput; diff --git a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx index 5d9e8459f4..0b80169927 100644 --- a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx +++ b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import {SectionCustomComponentProps} from '@models/navigator'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; -import {openPreviewConfigurationEditor} from '@redux/reducers/ui'; +import {openPreviewConfigurationEditor} from '@redux/reducers/main'; import Colors from '@styles/Colors'; @@ -35,7 +35,7 @@ const PreviewConfigurationNameSuffix: React.FC = pr const dispatch = useAppDispatch(); const onClick = () => { - dispatch(openPreviewConfigurationEditor()); + dispatch(openPreviewConfigurationEditor(sectionInstance.id.replace('-configurations', ''))); }; return ( diff --git a/src/redux/initialState.ts b/src/redux/initialState.ts index d5d92731cf..8e8c1870f6 100644 --- a/src/redux/initialState.ts +++ b/src/redux/initialState.ts @@ -46,6 +46,9 @@ const initialAppState: AppState = { shouldEditorReloadSelectedPath: false, checkedResourceIds: [], registeredKindHandlers: [], + prevConfEditor: { + isOpen: false, + }, }; const initialAppConfigState: AppConfig = { @@ -101,7 +104,6 @@ const initialUiState: UiState = { isClusterDiffVisible: false, isNotificationsOpen: false, isFolderLoading: false, - isPreviewConfigurationEditorOpen: false, quickSearchActionsPopup: { isOpen: false, }, diff --git a/src/redux/reducers/main.ts b/src/redux/reducers/main.ts index fa7fc370e9..f97ea37718 100644 --- a/src/redux/reducers/main.ts +++ b/src/redux/reducers/main.ts @@ -696,6 +696,19 @@ export const mainSlice = createSlice({ notification.hasSeen = true; }); }, + openPreviewConfigurationEditor: (state: Draft, action: PayloadAction) => { + const helmChartId = action.payload; + state.prevConfEditor = { + helmChartId, + isOpen: true, + }; + }, + closePreviewConfigurationEditor: (state: Draft) => { + state.prevConfEditor = { + isOpen: false, + helmChartId: undefined, + }; + }, }, extraReducers: builder => { builder.addCase(setAlert, (state, action) => { @@ -1141,5 +1154,7 @@ export const { addMultipleKindHandlers, addKindHandler, seenNotifications, + openPreviewConfigurationEditor, + closePreviewConfigurationEditor, } = mainSlice.actions; export default mainSlice.reducer; diff --git a/src/redux/reducers/ui.ts b/src/redux/reducers/ui.ts index 9285a4dbc7..c43d94bbb6 100644 --- a/src/redux/reducers/ui.ts +++ b/src/redux/reducers/ui.ts @@ -223,12 +223,6 @@ export const uiSlice = createSlice({ state.highlightedItems.browseTemplates = action.payload === HighlightItems.BROWSE_TEMPLATES; state.highlightedItems.connectToCluster = action.payload === HighlightItems.CONNECT_TO_CLUSTER; }, - openPreviewConfigurationEditor: (state: Draft) => { - state.isPreviewConfigurationEditorOpen = true; - }, - closePreviewConfigurationEditor: (state: Draft) => { - state.isPreviewConfigurationEditorOpen = false; - }, }, extraReducers: builder => { builder @@ -294,7 +288,5 @@ export const { closeSaveResourcesToFileFolderModal, zoomIn, zoomOut, - openPreviewConfigurationEditor, - closePreviewConfigurationEditor, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000000..db6bb40273 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,23 @@ +/** + * Mutates an array by moving an element from an index to another + * @param array + * @param fromIndex + * @param toIndex + */ +export function arrayMoveMutate(array: T[], fromIndex: number, toIndex: number) { + const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex; + if (startIndex >= 0 && startIndex < array.length) { + const endIndex = toIndex < 0 ? array.length + toIndex : toIndex; + const [element] = array.splice(fromIndex, 1); + array.splice(endIndex, 0, element); + } +} + +/** + * Returns a copy of the array with an element moved from an index to another + */ +export function arrayMove(array: T[], fromIndex: number, toIndex: number) { + const arrayCopy = array.slice(); + arrayMoveMutate(arrayCopy, fromIndex, toIndex); + return arrayCopy; +} From c814d1cfb49d97b807b83f113deaed28545e1155 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Fri, 18 Feb 2022 15:47:23 +0200 Subject: [PATCH 7/9] feat: add prev conf action buttons --- .../PreviewConfigurationEditor.tsx | 11 ++++++++++- .../organisms/PreviewConfigurationEditor/styled.tsx | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx index 7e7725373f..3fd0d3262d 100644 --- a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -1,6 +1,6 @@ import {useState} from 'react'; -import {Input, Select} from 'antd'; +import {Button, Input, Select} from 'antd'; import {helmInstallOptions, helmTemplateOptions} from '@constants/helmOptions'; @@ -72,6 +72,15 @@ const PreviewConfigurationEditor = () => { onChange={setHelmOptions} /> + + + + +
); }; diff --git a/src/components/organisms/PreviewConfigurationEditor/styled.tsx b/src/components/organisms/PreviewConfigurationEditor/styled.tsx index 68a7db3edb..6b30367f77 100644 --- a/src/components/organisms/PreviewConfigurationEditor/styled.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/styled.tsx @@ -20,3 +20,10 @@ export const Description = styled.p` font-size: 14px; color: ${Colors.grey7}; `; + +export const ActionsContainer = styled.div` + margin-top: 12px; + display: flex; + justify-content: flex-end; + gap: 8px; +`; From 7fbfcce52ba70a8d24eced785cdfe08b4f133f35 Mon Sep 17 00:00:00 2001 From: Catalin <20538711+devcatalin@users.noreply.github.com> Date: Sat, 19 Feb 2022 20:19:58 +0200 Subject: [PATCH 8/9] chore: add new prev conf tooltip constant --- src/constants/tooltips.ts | 1 + .../PreviewConfigurationQuickAction.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/constants/tooltips.ts b/src/constants/tooltips.ts index 8b772b4635..f4ee38df3c 100644 --- a/src/constants/tooltips.ts +++ b/src/constants/tooltips.ts @@ -56,3 +56,4 @@ export const SearchProjectTooltip = 'Search for project by name or path'; export const PluginDrawerTooltip = 'Open Plugins Manager'; export const QuickFilterTooltip = `Filter results – Hint: quick-filter using ${KEY_CTRL_CMD} + P`; export const NewResourceTooltip = `Create new resource (${KEY_CTRL_CMD} + N)`; +export const NewPreviewConfigurationTooltip = 'Create a new Preview Configuration'; diff --git a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx index 0b80169927..3e521b98e0 100644 --- a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx +++ b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx @@ -6,6 +6,8 @@ import {PlusOutlined} from '@ant-design/icons'; import styled from 'styled-components'; +import {NewPreviewConfigurationTooltip} from '@constants/tooltips'; + import {SectionCustomComponentProps} from '@models/navigator'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; @@ -41,7 +43,7 @@ const PreviewConfigurationNameSuffix: React.FC = pr return ( - + + {docsUrl && ( + + )} {entries.map(entry => ( { const [helmOptions, setHelmOptions] = useState({}); const [helmCommand, setHelmCommand] = useState<'template' | 'install'>(helmPreviewMode || 'template'); - const keyValueInputSchema = helmCommand === 'template' ? helmTemplateOptions : helmInstallOptions; + const keyValueInputSchema = useMemo( + () => (helmCommand === 'template' ? helmTemplateOptions : helmInstallOptions), + [helmCommand] + ); + + const helmOptionsDocsUrl = useMemo( + () => (helmCommand === 'template' ? HELM_TEMPLATE_OPTIONS_DOCS_URL : HELM_INSTALL_OPTIONS_DOCS_URL), + [helmCommand] + ); if (!helmChart) { return

Something went wrong, could not find the helm chart.

; @@ -69,6 +78,7 @@ const PreviewConfigurationEditor = () => { value={helmOptions} schema={keyValueInputSchema} data={{}} + docsUrl={helmOptionsDocsUrl} onChange={setHelmOptions} /> diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 48b78b77e0..b7295ffd84 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -30,3 +30,5 @@ export const DEFAULT_PLUGINS = [ export const PLUGIN_DOCS_URL = 'https://kubeshop.github.io/monokle/plugins/'; export const HELM_CHART_ENTRY_FILE = 'Chart.yaml'; export const HELM_CHART_SECTION_NAME = 'Helm Charts'; +export const HELM_TEMPLATE_OPTIONS_DOCS_URL = 'https://helm.sh/docs/helm/helm_template/#options'; +export const HELM_INSTALL_OPTIONS_DOCS_URL = 'https://helm.sh/docs/helm/helm_install/#options';