diff --git a/src/App.tsx b/src/App.tsx index 9316885ab6..f21e79c3ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ 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 {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'; @@ -53,6 +54,7 @@ const SaveResourceToFileFolderModal = React.lazy(() => import('@molecules/SaveRe const SettingsManager = React.lazy(() => import('@organisms/SettingsManager')); const StartupModal = React.lazy(() => import('@organisms/StartupModal')); const UpdateModal = React.lazy(() => import('@organisms/UpdateModal')); +const PreviewConfigurationEditor = React.lazy(() => import('@components/organisms/PreviewConfigurationEditor')); const AppContainer = styled.div` height: 100%; @@ -72,6 +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.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); @@ -239,6 +242,10 @@ const App = () => { dispatch(toggleSettings()); }; + const previewConfigurationDrawerOnClose = () => { + dispatch(closePreviewConfigurationEditor()); + }; + return ( @@ -268,6 +275,14 @@ const App = () => { + + + + {isChangeFiltersConfirmModalVisible && } {isClusterDiffModalVisible && } diff --git a/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx new file mode 100644 index 0000000000..f81466386c --- /dev/null +++ b/src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import {Select} from 'antd'; + +import {MinusOutlined} from '@ant-design/icons'; + +import Colors from '@styles/Colors'; + +import ValueInput from './ValueInput'; +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[]; +}; + +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..d22efba80d 100644 --- a/src/components/atoms/KeyValueInput/KeyValueInput.tsx +++ b/src/components/atoms/KeyValueInput/KeyValueInput.tsx @@ -1,62 +1,33 @@ -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 {openUrlInExternalBrowser} from '@utils/shell'; + +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; + docsUrl?: string; + 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,24 +42,26 @@ 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, docsUrl, 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; } + 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({ @@ -100,12 +73,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 = () => { @@ -125,10 +98,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); @@ -145,62 +121,49 @@ 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} - + + {docsUrl && ( + + )} {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/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; 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/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/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 = () => { { diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx b/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx index 211acbcffb..0714767b5a 100644 --- a/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx +++ b/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx @@ -85,6 +85,7 @@ function ItemRenderer(props: ItemRendererProps {itemInstance.isCheckable && (blueprint.customization?.isCheckVisibleOnHover ? itemInstance.isChecked || isHovered : true) && ( diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx b/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx index 3027de8798..981d1addfc 100644 --- a/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx +++ b/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx @@ -15,6 +15,7 @@ type ItemContainerProps = { $indentation: number; $isSectionCheckable: boolean; $hasCustomNameDisplay: boolean; + $lastItemMarginBottom?: number; }; export const ItemContainer = styled.span` @@ -31,6 +32,9 @@ export const ItemContainer = styled.span` ${props => props.hasOnClick && `cursor: pointer;`} ${props => { if (props.isLastItem) { + if (props.$lastItemMarginBottom !== undefined) { + return `margin-bottom: ${props.$lastItemMarginBottom}px;`; + } return `margin-bottom: 12px;`; } }} diff --git a/src/components/molecules/SectionRenderer/SectionHeader.tsx b/src/components/molecules/SectionRenderer/SectionHeader.tsx index c0c34c9827..eb4e735d46 100644 --- a/src/components/molecules/SectionRenderer/SectionHeader.tsx +++ b/src/components/molecules/SectionRenderer/SectionHeader.tsx @@ -85,6 +85,7 @@ function SectionHeader(props: SectionHeaderProps) { hasCustomNameDisplay={Boolean(NameDisplay.Component)} isLastSection={isLastSection} isCollapsed={isCollapsed} + $marginBottom={sectionBlueprint.customization?.sectionMarginBottom} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > @@ -131,7 +132,9 @@ function SectionHeader(props: SectionHeaderProps) { > {name} - {counter && {counter}} + {counter !== undefined && ( + {counter} + )} {NameSuffix.Component && (NameSuffix.options?.isVisibleOnHover ? isHovered : true) && ( diff --git a/src/components/molecules/SectionRenderer/SectionRenderer.tsx b/src/components/molecules/SectionRenderer/SectionRenderer.tsx index 2154f00178..ad7d4bb066 100644 --- a/src/components/molecules/SectionRenderer/SectionRenderer.tsx +++ b/src/components/molecules/SectionRenderer/SectionRenderer.tsx @@ -219,7 +219,7 @@ function SectionRenderer(props: SectionRendererProps) { itemId={itemId} blueprint={itemBlueprint} level={level + 1} - isLastItem={isLastVisibleItemId(itemId) && !childSectionIds} + isLastItem={isLastVisibleItemId(itemId)} isSectionCheckable={Boolean(sectionInstance.checkable)} sectionContainerElementId={sectionBlueprint.containerElementId} options={itemRendererOptions} diff --git a/src/components/molecules/SectionRenderer/styled.tsx b/src/components/molecules/SectionRenderer/styled.tsx index ff1d9d664a..0c38ab5b39 100644 --- a/src/components/molecules/SectionRenderer/styled.tsx +++ b/src/components/molecules/SectionRenderer/styled.tsx @@ -11,6 +11,18 @@ type NameContainerProps = { isCheckable?: boolean; }; +export const NameContainer = styled.span` + display: flex; + align-items: center; + width: 100%; + ${props => { + const defaultIndentation = props.isCheckable ? 24 : 0; + return `padding-left: ${defaultIndentation + props.$indentation}px;`; + }} + ${props => !props.isHovered && 'padding-right: 30px;'} + ${props => props.$hasCustomNameDisplay && 'padding: 0;'} +`; + type SectionContainerProps = { isSelected?: boolean; isHighlighted?: boolean; @@ -23,20 +35,9 @@ type SectionContainerProps = { disableHoverStyle?: boolean; isSectionCheckable?: boolean; hasCustomNameDisplay?: boolean; + $marginBottom?: number; }; -export const NameContainer = styled.span` - display: flex; - align-items: center; - width: 100%; - ${props => { - const defaultIndentation = props.isCheckable ? 24 : 0; - return `padding-left: ${defaultIndentation + props.$indentation}px;`; - }} - ${props => !props.isHovered && 'padding-right: 30px;'} - ${props => props.$hasCustomNameDisplay && 'padding: 0;'} -`; - export const SectionContainer = styled.li` display: flex; justify-content: space-between; @@ -77,6 +78,7 @@ export const SectionContainer = styled.li` return `background: ${Colors.blackPearl};`; } }}; + ${props => props.$marginBottom && `margin-bottom: ${props.$marginBottom}px;`} `; type NameProps = { diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx new file mode 100644 index 0000000000..63d81de379 --- /dev/null +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -0,0 +1,98 @@ +import {useMemo, useState} from 'react'; + +import {Button, Input, Select} from 'antd'; + +import {HELM_INSTALL_OPTIONS_DOCS_URL, HELM_TEMPLATE_OPTIONS_DOCS_URL} from '@constants/constants'; +import {helmInstallOptions, helmTemplateOptions} from '@constants/helmOptions'; + +import {HelmValuesFile} from '@models/helm'; + +import {useAppSelector} from '@redux/hooks'; + +import {KeyValueInput, OrderedList} from '@components/atoms'; +import {OrderedListItem} from '@components/atoms/OrderedList'; + +import * as S from './styled'; + +const PreviewConfigurationEditor = () => { + const helmPreviewMode = useAppSelector( + 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 = 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.

; + } + + return ( +
+ + Name your configuration: + + + + Select which values files to use: + Drag and drop to specify order + + + + Select which helm command to use for this Preview: + + + + + + + + + + +
+ ); +}; + +export default PreviewConfigurationEditor; diff --git a/src/components/organisms/PreviewConfigurationEditor/index.ts b/src/components/organisms/PreviewConfigurationEditor/index.ts new file mode 100644 index 0000000000..3f910318b4 --- /dev/null +++ b/src/components/organisms/PreviewConfigurationEditor/index.ts @@ -0,0 +1 @@ +export {default} from './PreviewConfigurationEditor'; diff --git a/src/components/organisms/PreviewConfigurationEditor/styled.tsx b/src/components/organisms/PreviewConfigurationEditor/styled.tsx new file mode 100644 index 0000000000..6b30367f77 --- /dev/null +++ b/src/components/organisms/PreviewConfigurationEditor/styled.tsx @@ -0,0 +1,29 @@ +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}; +`; + +export const ActionsContainer = styled.div` + margin-top: 12px; + display: flex; + justify-content: flex-end; + gap: 8px; +`; 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'; diff --git a/src/constants/helmOptions.ts b/src/constants/helmOptions.ts new file mode 100644 index 0000000000..cd8698d875 --- /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': 'string', + '--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', +}; 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/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/navigator.ts b/src/models/navigator.ts index 14ecb509bf..bf2231a166 100644 --- a/src/models/navigator.ts +++ b/src/models/navigator.ts @@ -39,6 +39,7 @@ export interface ItemCustomization { }; disableHoverStyle?: boolean; isCheckVisibleOnHover?: boolean; + lastItemMarginBottom?: number; } export type SectionCustomComponentProps = { @@ -80,6 +81,7 @@ export interface SectionCustomization { disableHoverStyle?: boolean; beforeInitializationText?: string; isCheckVisibleOnHover?: boolean; + sectionMarginBottom?: number; } export interface ItemBlueprint { diff --git a/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts b/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts index cf7411ff81..1b80feba34 100644 --- a/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts +++ b/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts @@ -11,6 +11,7 @@ import Colors from '@styles/Colors'; import CollapseSectionPrefix from './CollapseSectionPrefix'; import FileItemPrefix from './FileItemPrefix'; import HelmChartQuickAction from './HelmChartQuickAction'; +import PreviewConfigurationNameSuffix from './PreviewConfigurationQuickAction'; export type ValuesFilesScopeType = { helmValuesMap: HelmValuesMapType; @@ -28,6 +29,39 @@ type HelmChartScopeType = { }; export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { + // TODO: replace 'any' type after implementing the state for preview configurations + const previewConfigurationsSectionBlueprint: SectionBlueprint = { + name: 'Preview Configurations', + id: `${helmChart.id}-configurations`, + containerElementId: 'helm-section-container', + rootSectionId: HELM_CHART_SECTION_NAME, + getScope: () => { + return {}; + }, + builder: { + isInitialized: () => true, + isVisible: () => true, + }, + customization: { + counterDisplayMode: 'items', + indentation: 10, + nameWeight: 400, + nameSize: 14, + nameColor: Colors.grey9, + nameHorizontalPadding: 0, + namePrefix: { + component: CollapseSectionPrefix, + }, + sectionMarginBottom: 12, + nameSuffix: { + component: PreviewConfigurationNameSuffix, + options: { + isVisibleOnHover: true, + }, + }, + }, + }; + const valuesFilesSectionBlueprint: SectionBlueprint = { name: 'Values Files', id: `${helmChart.id}-values`, @@ -99,6 +133,7 @@ export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { prefix: { component: FileItemPrefix, }, + lastItemMarginBottom: 0, }, }, }; @@ -108,7 +143,7 @@ export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { name: helmChart.name, containerElementId: 'helm-sections-container', rootSectionId: HELM_CHART_SECTION_NAME, - childSectionIds: [valuesFilesSectionBlueprint.id], + childSectionIds: [valuesFilesSectionBlueprint.id, previewConfigurationsSectionBlueprint.id], getScope: state => { const kubeConfigPath = state.config.projectConfig?.kubeConfig?.path || state.config.kubeConfig.path; return { @@ -160,6 +195,7 @@ export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { }, customization: { prefix: {component: FileItemPrefix}, + lastItemMarginBottom: 0, }, }, customization: { @@ -171,5 +207,5 @@ export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { }, }; - return {helmChartSectionBlueprint, valuesFilesSectionBlueprint}; + return {helmChartSectionBlueprint, valuesFilesSectionBlueprint, previewConfigurationsSectionBlueprint}; } diff --git a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx new file mode 100644 index 0000000000..3e521b98e0 --- /dev/null +++ b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import {Button, Tooltip} from 'antd'; + +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'; +import {openPreviewConfigurationEditor} from '@redux/reducers/main'; + +import Colors from '@styles/Colors'; + +const SuffixContainer = styled.span` + display: inline-block; +`; + +const ButtonContainer = styled.span` + display: flex; + align-items: center; + padding: 0 4px; + margin-right: 2px; + & .ant-btn-sm { + height: 20px; + width: 20px; + } +`; + +const PreviewConfigurationNameSuffix: React.FC = props => { + const {sectionInstance} = props; + const isSectionCollapsed = useAppSelector(state => state.navigator.collapsedSectionIds.includes(sectionInstance.id)); + + const dispatch = useAppDispatch(); + + const onClick = () => { + dispatch(openPreviewConfigurationEditor(sectionInstance.id.replace('-configurations', ''))); + }; + + return ( + + + +