Skip to content

Commit

Permalink
Merge pull request #1379 from kubeshop/devcatalin/feat/preview-config…
Browse files Browse the repository at this point in the history
…uration-section

PvConf#1 - feat: add preview configurations sections
  • Loading branch information
devcatalin authored Feb 19, 2022
2 parents 87fb75c + 5876fd9 commit 5fb380d
Show file tree
Hide file tree
Showing 33 changed files with 673 additions and 131 deletions.
15 changes: 15 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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%;
Expand All @@ -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);
Expand Down Expand Up @@ -239,6 +242,10 @@ const App = () => {
dispatch(toggleSettings());
};

const previewConfigurationDrawerOnClose = () => {
dispatch(closePreviewConfigurationEditor());
};

return (
<AppContext.Provider value={{windowSize: size}}>
<AppContainer>
Expand Down Expand Up @@ -268,6 +275,14 @@ const App = () => {
<SettingsManager />
</LazyDrawer>

<LazyDrawer
onClose={previewConfigurationDrawerOnClose}
title="Preview Configuration"
visible={isPreviewConfigurationEditorOpen}
>
<PreviewConfigurationEditor />
</LazyDrawer>

<Suspense fallback={null}>
{isChangeFiltersConfirmModalVisible && <ChangeFiltersConfirmModal />}
{isClusterDiffModalVisible && <ClusterDiffModal />}
Expand Down
60 changes: 60 additions & 0 deletions src/components/atoms/KeyValueInput/KeyValueEntryRenderer.tsx
Original file line number Diff line number Diff line change
@@ -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<KeyValueEntryRendererProps> = props => {
const {entry, valueType, onKeyChange, onValueChange, onEntryRemove, disabled, availableKeys, availableValues} = props;

return (
<S.KeyValueRemoveButtonContainer key={entry.id}>
<S.KeyValueContainer>
<Select value={entry.key} onChange={onKeyChange} showSearch disabled={disabled}>
{availableKeys.map(key => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
</Select>

{entry.key && valueType && valueType !== 'boolean' && (
<ValueInput
value={entry.value}
valueType={valueType}
availableValues={availableValues}
onChange={onValueChange}
/>
)}
</S.KeyValueContainer>

<S.StyledRemoveButton
disabled={disabled}
onClick={() => onEntryRemove(entry.id)}
color={Colors.redError}
size="small"
icon={<MinusOutlined />}
/>
</S.KeyValueRemoveButtonContainer>
);
};

export default KeyValueEntryRenderer;
175 changes: 69 additions & 106 deletions src/components/atoms/KeyValueInput/KeyValueInput.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string | null>;
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<string, string>;
data: Record<string, string[]>;
value: KeyValue;
onChange: (keyValues: KeyValue) => void;
value: KeyValueData;
docsUrl?: string;
onChange: (keyValueData: KeyValueData) => void;
};

export const ANY_VALUE = '<any>';

function makeKeyValueFromEntries(keyValueEntries: KeyValueEntry[]): KeyValue {
const keyValue: KeyValue = {};
function makeKeyValueDataFromEntries(keyValueEntries: KeyValueEntry[]): KeyValueData {
const keyValue: KeyValueData = {};
keyValueEntries.forEach(({key, value}) => {
if (!key || !value) {
return;
Expand All @@ -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<KeyValueEntry[]>([]);
const [currentKeyValue, setCurrentKeyValue] = useState<KeyValue>(keyValue);
const [currentKeyValueData, setCurrentKeyValueData] = useState<KeyValueData>(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({
Expand All @@ -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 = () => {
Expand All @@ -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);
Expand All @@ -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 (
<Container>
<TitleContainer>
<TitleLabel style={labelStyle}>{label}</TitleLabel>
<S.Container>
<S.TitleContainer>
<S.TitleLabel style={labelStyle}>{label}</S.TitleLabel>
<Button onClick={createEntry} type="link" icon={<PlusOutlined />} disabled={disabled}>
Add
</Button>
</TitleContainer>
</S.TitleContainer>
{docsUrl && (
<Button type="link" onClick={() => openUrlInExternalBrowser(docsUrl)} style={{padding: 0}}>
Documentation
</Button>
)}
{entries.map(entry => (
<KeyValueRemoveButtonContainer key={entry.id}>
<KeyValueContainer>
<Select
value={entry.key}
onChange={newKey => updateEntryKey(entry.id, newKey)}
showSearch
disabled={disabled}
>
{Object.keys(data)
.filter(key => key === entry.key || !entries.some(e => e.key === key))
.map(key => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
</Select>

{entry.key && (
<Select
value={entry.value}
onChange={newValue => updateEntryValue(entry.id, newValue)}
showSearch
disabled={disabled}
>
<Select.Option key={ANY_VALUE} value={ANY_VALUE}>
{ANY_VALUE}
</Select.Option>
{data[entry.key] &&
data[entry.key].map((value: string) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
))}
</Select>
)}
</KeyValueContainer>

<StyledRemoveButton
disabled={disabled}
onClick={() => removeEntry(entry.id)}
color={Colors.redError}
size="small"
icon={<MinusOutlined />}
/>
</KeyValueRemoveButtonContainer>
<KeyValueEntryRenderer
key={entry.id}
entry={entry}
valueType={entry.key ? schema[entry.key] : undefined}
onKeyChange={newKey => updateEntryKey(entry.id, newKey)}
onValueChange={newValue => updateEntryValue(entry.id, newValue)}
onEntryRemove={removeEntry}
availableKeys={getEntryAvailableKeys(entry)}
availableValues={getEntryAvailableValues(entry)}
/>
))}
</Container>
</S.Container>
);
}

Expand Down
39 changes: 39 additions & 0 deletions src/components/atoms/KeyValueInput/ValueInput.tsx
Original file line number Diff line number Diff line change
@@ -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<ValueInputProps> = 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 (
<Select value={value} onChange={onChange} showSearch disabled={disabled}>
<Select.Option key={ANY_VALUE} value={ANY_VALUE}>
{ANY_VALUE}
</Select.Option>
{availableValues?.map((valueOption: string) => (
<Select.Option key={valueOption} value={valueOption}>
{valueOption}
</Select.Option>
))}
</Select>
);
}
return <Input value={value} onChange={e => onChange(e.target.value)} disabled={disabled} />;
}

// TODO: decide if we want to implement more value types
return null;
};

export default ValueInput;
1 change: 1 addition & 0 deletions src/components/atoms/KeyValueInput/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ANY_VALUE = '<any>';
Loading

0 comments on commit 5fb380d

Please sign in to comment.