Skip to content

Commit

Permalink
feat(metadata): Metadata configuration UI
Browse files Browse the repository at this point in the history
  • Loading branch information
igarashitm committed Jun 16, 2023
1 parent f0a988f commit 4400fb2
Show file tree
Hide file tree
Showing 16 changed files with 1,052 additions and 1 deletion.
33 changes: 33 additions & 0 deletions src/api/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,39 @@ export async function fetchIntegrationSourceCode(flowsWrapper: IFlowsWrapper, na
}
}

/**
* @todo Fetch this from backend
* @param dsl
*/
export async function fetchMetadataSchema(dsl: string): Promise<{ [key: string]: any }> {
if (['Kamelet', 'Camel Route', 'Integration'].includes(dsl)) {
return Promise.resolve({
beans: {
title: 'Beans',
description: 'Beans Configuration',
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
properties: {
type: 'object',
},
},
required: ['name', 'type'],
},
},
});
}
return Promise.resolve({});
}

export async function fetchStepDetails(id?: string, namespace?: string) {
try {
const resp = await RequestService.get({
Expand Down
5 changes: 5 additions & 0 deletions src/components/KaotoToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DeploymentsModal } from './DeploymentsModal';
import { ExportCanvasToPng } from './ExportCanvasToPng';
import { FlowsMenu } from './Flows/FlowsMenu';
import { SettingsModal } from './SettingsModal';
import { MetadataToolbarItems } from './metadata/MetadataToolbarItems';
import { fetchDefaultNamespace, startDeployment } from '@kaoto/api';
import { LOCAL_STORAGE_UI_THEME_KEY, THEME_DARK_CLASS } from '@kaoto/constants';
import {
Expand Down Expand Up @@ -325,6 +326,10 @@ export const KaotoToolbar = ({

<ToolbarItem variant="separator" />

<MetadataToolbarItems />

<ToolbarItem variant="separator" />

{/* DEPLOYMENT STATUS */}
{deployment.crd ? (
<ToolbarItem alignment={{ default: 'alignRight' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Visualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const Visualization = () => {
} else {
fetchTheSourceCode({ flows, properties, metadata }, settings);
}
}, [flows, properties]);
}, [flows, properties, metadata]);

const fetchTheSourceCode = (currentFlowsWrapper: IFlowsWrapper, settings: ISettings) => {
const updatedFlowWrapper = {
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './VisualizationControls';
export * from './VisualizationStepViews';
export * from './Visualization';
export * from './VisualizationStep';
export { MetadataToolbarItems } from './metadata/MetadataToolbarItems';
174 changes: 174 additions & 0 deletions src/components/metadata/AddPropertyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
Button,
FormGroup,
HelperText,
HelperTextItem,
Popover,
Radio,
Split,
SplitItem,
Stack,
StackItem,
TextInput,
Title,
Tooltip,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { useState } from 'react';

type AddPropertyPopoverProps = {
textLabel?: string;
model: any;
path: string[];
disabled?: boolean;
onChangeModel: (model: any) => void;
};

/**
* Add property button shows a popover to receive user inputs for new property name and type,
* as well as to validate if the property name already exists before actually adding it
* into the model object.
* @param props
* @constructor
*/
export function AddPropertyButton({
textLabel = '',
model,
path,
disabled = false,
onChangeModel,
}: AddPropertyPopoverProps) {
const [isVisible, doSetVisible] = useState<boolean>(false);
const [propertyType, setPropertyType] = useState<'string' | 'object'>('string');
const [propertyName, setPropertyName] = useState<string>('');
const [propertyValue, setPropertyValue] = useState<string>('');

function isReadyToAdd() {
return !!(propertyName && model[propertyName] == null);
}

function isDuplicate() {
if (!model || !propertyName) {
return false;
}
return model[propertyName] != null;
}
function setVisible(visible: boolean) {
if (!model) {
onChangeModel({});
}
doSetVisible(visible);
}

function handleAddProperty() {
if (propertyType === 'object') {
model[propertyName] = {};
} else {
model[propertyName] = propertyValue;
}
onChangeModel(model);
setPropertyName('');
setPropertyValue('');
setPropertyType('string');
setVisible(false);
}

return (
<Popover
aria-label={`add-property-popover-${path.join('-')}`}
data-testid={`properties-add-property-${path.join('-')}-popover`}
isVisible={isVisible}
shouldOpen={() => setVisible(true)}
shouldClose={() => setVisible(false)}
bodyContent={
<Stack hasGutter>
<StackItem>
<FormGroup>
<Title headingLevel="h4">Name</Title>
<TextInput
name="property-name"
aria-label={`properties-add-property-${path.join('-')}-name-input`}
data-testid={`properties-add-property-${path.join('-')}-name-input`}
aria-invalid={isDuplicate()}
value={propertyName}
onChange={(value) => setPropertyName(value)}
/>
{isDuplicate() && (
<HelperText>
<HelperTextItem variant="error">
Please specify a unique property name
</HelperTextItem>
</HelperText>
)}
</FormGroup>
</StackItem>
<StackItem>
<Split hasGutter>
<SplitItem>
<FormGroup isInline>
<Split hasGutter>
<SplitItem>
<Radio
name="property-type"
label="String"
id={`properties-add-property-${path.join('-')}-type-string`}
data-testid={`properties-add-property-${path.join('-')}-type-string`}
isChecked={propertyType === 'string'}
onChange={(checked) => checked && setPropertyType('string')}
/>
</SplitItem>
<SplitItem>
<Radio
name="property-type"
label="Object"
id={`properties-add-property-${path.join('-')}-type-object`}
data-testid={`properties-add-property-${path.join('-')}-type-object`}
isChecked={propertyType === 'object'}
onChange={(checked) => checked && setPropertyType('object')}
/>
</SplitItem>
</Split>
</FormGroup>
</SplitItem>
</Split>
</StackItem>
<StackItem>
<FormGroup>
<Title headingLevel={'h4'}>Value</Title>
<TextInput
name="property-value"
aria-label={`properties-add-property-${path.join('-')}-value-input`}
data-testid={`properties-add-property-${path.join('-')}-value-input`}
value={propertyValue}
isDisabled={propertyType === 'object'}
onChange={(value) => setPropertyValue(value)}
/>
</FormGroup>
</StackItem>
<StackItem>
<Button
aria-label={`properties-add-property-${path.join('-')}-add-btn`}
data-testid={`properties-add-property-${path.join('-')}-add-btn`}
variant="primary"
onClick={handleAddProperty}
isDisabled={!isReadyToAdd()}
>
Add
</Button>
</StackItem>
</Stack>
}
>
<Tooltip content="Add property">
<Button
data-testid={`properties-add-property-${path.join('-')}-popover-btn`}
variant={'link'}
icon={<PlusCircleIcon />}
isDisabled={disabled}
>
{textLabel}
</Button>
</Tooltip>
</Popover>
);
}
17 changes: 17 additions & 0 deletions src/components/metadata/MetadataEditorBridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import PropertiesField from './PropertiesField';
import JSONSchemaBridge from 'uniforms-bridge-json-schema';

/**
* Add {@link PropertiesField} custom field for adding generic properties editor.
*/
export class MetadataEditorBridge extends JSONSchemaBridge {
getField(name: string): Record<string, any> {
const field = super.getField(name);
if (field.type === 'object' && !field.properties) {
field.uniforms = {
component: PropertiesField,
};
}
return field;
}
}
9 changes: 9 additions & 0 deletions src/components/metadata/MetadataEditorModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.metadataEditorModal {
--pf-c-modal-box--ZIndex: 500;
--pf-c-modal-box--Height: 90vh;
--pf-c-modal-box--Width: 90vw;
--pf-c-modal-box__body--MinHeight: 90vh;
--pf-c-modal-box__body--MaxHeight: 90vh;
--pf-c-modal-box__body--MinWidth: 90vw;
--pf-c-modal-box__body--MaxWidth: 90vw;
}
83 changes: 83 additions & 0 deletions src/components/metadata/MetadataEditorModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { MetadataEditorModal } from './MetadataEditorModal';
import { useArgs } from '@storybook/client-api';
import { StoryFn, Meta } from '@storybook/react';

export default {
title: 'Metadata/MetadataEditorModal',
component: MetadataEditorModal,
excludeStories: ['schemaMock'],
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
argTypes: { handleCloseModal: { action: 'clicked' } },
} as Meta<typeof MetadataEditorModal>;

const Template: StoryFn<typeof MetadataEditorModal> = (args) => {
const [{ isModalOpen }, updateArgs] = useArgs();
const handleClose = () => updateArgs({ isModalOpen: !isModalOpen });
return (
<>
<button onClick={() => updateArgs({ isModalOpen: !isModalOpen })}>
Open Metadata Editor Modal
</button>
<MetadataEditorModal {...args} handleCloseModal={handleClose} />
</>
);
};

export const schemaMock = {
beans: {
title: 'Beans',
description: 'Beans Configuration',
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
properties: {
type: 'object',
},
},
required: ['name', 'type'],
},
},
single: {
title: 'Single Object',
description: 'Single Object Configuration',
type: 'object',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
properties: {
type: 'object',
},
},
},
};

export const BeansArray = Template.bind({});
BeansArray.args = {
name: 'beans',
schema: schemaMock.beans,
};

export const SingleObject = Template.bind({});
SingleObject.args = {
name: 'singleObject',
schema: schemaMock.single,
};
Loading

0 comments on commit 4400fb2

Please sign in to comment.