Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CredentialType - update Generate extra vars #2194

Closed
wants to merge 8 commits into from
2 changes: 1 addition & 1 deletion framework/PageForm/Inputs/PageFormDataEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export function valueToObject(
}
}

function objectToString(obj: object, language: DataEditorLanguages): string {
export function objectToString(obj: object, language: DataEditorLanguages): string {
if (obj === null || obj === undefined) {
return '';
}
Expand Down
104 changes: 104 additions & 0 deletions frontend/eda/access/credential-types/CredentialTypeForm.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { CreateCredentialType, EditCredentialType } from './CredentialTypeForm';
import { EdaCredentialType } from '../../interfaces/EdaCredentialType';
import { edaAPI } from '../../common/eda-utils';
describe('CredentialTypeForm.cy.ts', () => {
const credentialType = {
name: 'Sample Credential Type',
description: 'This is a sample credential',
id: 1,
inputs: {
fields: [
{
id: 'name',
type: 'string',
label: 'Name',
},
],
required: ['name'],
},
injectors: {},
};

describe('Create Credential Type', () => {
it('should validate required fields on save', () => {
cy.mount(<CreateCredentialType />);
cy.clickButton(/^Create credential type$/);
cy.contains('Name is required.').should('be.visible');
});

it('should create credential type', () => {
cy.intercept('POST', edaAPI`/credential-types`, {
statusCode: 201,
body: credentialType,
}).as('createCredentialType');
cy.mount(<CreateCredentialType />);
cy.get('[data-cy="name"]').type('New Credential Type');
cy.get('[data-cy="description"]').type('New credential description');
cy.clickButton(/^Create credential type$/);
cy.wait('@createCredentialType')
.its('request.body')
.then((createdCredentialType: EdaCredentialType) => {
expect(createdCredentialType).to.deep.equal({
name: 'New Credential Type',
description: 'New credential description',
inputs: {},
injectors: {},
});
});
});
});

describe('Edit Credential Type', () => {
beforeEach(() => {
cy.intercept(
{ method: 'GET', url: edaAPI`/credential-types/*` },
{ statusCode: 200, body: credentialType }
);
});

it('should preload the form with current values', () => {
cy.mount(<EditCredentialType />);
cy.verifyPageTitle('Edit Credential Type');
cy.get('[data-cy="name"]').should('have.value', 'Sample Credential Type');
cy.get('[data-cy="description"]').should('have.value', 'This is a sample credential');
cy.dataEditorShouldContain('[data-cy="inputs"]', credentialType.inputs);
cy.dataEditorShouldContain('[data-cy="injectors"]', credentialType.injectors);
});

it('should edit credential type', () => {
cy.intercept('PATCH', edaAPI`/credential-types/*`, {
statusCode: 201,
body: credentialType,
}).as('editCredentialType');
cy.mount(<EditCredentialType />);
cy.get('[data-cy="name"]').should('have.value', 'Sample Credential Type');
cy.get('[data-cy="name"]').clear();
cy.get('[data-cy="name"]').type('Modified Credential Type');
cy.clickButton(/^Save credential type$/);
cy.wait('@editCredentialType')
.its('request.body')
.then((editedCredentialType: EdaCredentialType) => {
expect(editedCredentialType.name).to.equal('Modified Credential Type');
expect(editedCredentialType.description).to.equal('This is a sample credential');
expect(editedCredentialType.inputs).to.deep.equal({
fields: [
{
id: 'name',
type: 'string',
label: 'Name',
},
],
required: ['name'],
});
});
});

it('should generate extra vars', () => {
cy.mount(<EditCredentialType />);
cy.get('[data-cy="name"]').type('Sample Credential Type');
cy.get('[data-cy="description"]').type('This is a sample credential');
cy.clickButton(/^Generate extra vars$/);
cy.dataEditorShouldContain('[data-cy="injectors"]', "extra_vars:\nname: '{{ name }}'");
});
});
});
40 changes: 17 additions & 23 deletions frontend/eda/access/credential-types/CredentialTypeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EdaPageForm } from '../../common/EdaPageForm';
import { Button } from '@patternfly/react-core';
import { useFormContext, useWatch } from 'react-hook-form';
import { useCallback } from 'react';
import { EdaPageFormDataEditor } from '../../common/EdaPageFormDataEditor';

export function CreateCredentialType() {
const { t } = useTranslation();
Expand Down Expand Up @@ -110,19 +111,14 @@ function CredentialTypeInputs() {
defaultValue: undefined,
}) as EdaCredentialTypeInputs;

const credentialInjectors = useWatch<EdaCredentialTypeCreate>({
name: 'injectors',
defaultValue: undefined,
}) as EdaCredentialTypeCreate;

const setInjectorsExtraVars = useCallback(() => {
const fields = credentialInputs?.fields;
let extraVarFields = '';
fields?.map((field, idx) => {
if (idx > 0) {
extraVarFields += ',';
}
extraVarFields += `"${field.id}" : "{{${field.id}}}"`;
extraVarFields += `"${field.id}" : "{{ ${field.id} }}"`;
});
const extraVars = `{"extra_vars": { ${extraVarFields}}}`;
setValue('injectors', JSON.parse(extraVars), { shouldValidate: true });
Expand Down Expand Up @@ -152,24 +148,22 @@ function CredentialTypeInputs() {
format="object"
/>
</PageFormSection>
{credentialInputs &&
Object.keys(credentialInputs).length !== 0 &&
(!credentialInjectors ||
(credentialInjectors && Object.keys(credentialInjectors).length === 0)) && (
<PageFormSection>
<Button
id={'generate-injector'}
variant={'primary'}
size={'sm'}
style={{ maxWidth: 150 }}
onClick={() => setInjectorsExtraVars()}
>
{t('Generate extra vars')}
</Button>
</PageFormSection>
)}
{credentialInputs && Object.keys(credentialInputs).length !== 0 && (
<PageFormSection>
<Button
id={'generate-injector'}
data-cy={'generate-injector'}
variant={'primary'}
size={'sm'}
style={{ maxWidth: 150 }}
onClick={() => setInjectorsExtraVars()}
>
{t('Generate extra vars')}
</Button>
</PageFormSection>
)}
<PageFormSection singleColumn>
<PageFormDataEditor
<EdaPageFormDataEditor
name="injectors"
label={t('Injector configuration')}
labelHelpTitle={t('Injector configuration')}
Expand Down
166 changes: 166 additions & 0 deletions frontend/eda/common/EdaPageFormDataEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { Controller, FieldPathByValue, FieldValues, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
DataEditorActions,
objectToString,
PageFormDataEditorInputProps,
usePageAlertToaster,
usePageSettings,
valueToObject,
} from '../../../framework';
import { useID } from '../../../framework/hooks/useID';
import { DataEditor, DataEditorLanguages } from '../../../framework/components/DataEditor';
import { useClipboard } from '../../../framework/hooks/useClipboard';
import { downloadTextFile } from '../../../framework/utils/download-file';
import { useRequiredValidationRule } from '../../../framework/PageForm/Inputs/validation-hooks';
import { PageFormGroup } from '../../../framework/PageForm/Inputs/PageFormGroup';
import { ExpandIcon } from '../../../framework/components/icons/ExpandIcon';
import { DropZone } from '../../../framework/components/DropZone';

export function EdaPageFormDataEditor<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPathByValue<
TFieldValues,
object | string | undefined | null
> = FieldPathByValue<TFieldValues, object | string | undefined | null>,
>(props: PageFormDataEditorInputProps<TFieldValues, TFieldName>) {
const { t } = useTranslation();
const {
name,
format: valueFormat,
disableCopy,
disableUpload,
disableDownload,
disableExpand,
validate,
isArray,
} = props;
const id = useID(props);
const {
formState: { isSubmitting, isValidating },
setError,
getValues,
clearErrors,
control,
} = useFormContext<TFieldValues>();
const settings = usePageSettings();
const defaultLanguage = settings.dataEditorFormat ?? 'yaml';
const [language, setLanguage] = useState<DataEditorLanguages>(defaultLanguage); // TODO settings.defaultCodeLanguage
const [isExpanded, setExpanded] = useState(!props.defaultCollapsed);

// Here we store the value the data editor is working with
const [dataEditorValue, setDataEditorValue] = useState<string>(() => {
const value = getValues(name);
if (typeof value === 'string') return value as string;
else return objectToString(value, defaultLanguage);
});

const alertToaster = usePageAlertToaster();
const { writeToClipboard } = useClipboard();

const handleCopy = useCallback(
() => writeToClipboard(objectToString(valueToObject(getValues(name), isArray), language)),
[getValues, isArray, language, name, writeToClipboard]
);

const onDrop = useCallback(
(contents: string) => {
setDataEditorValue(objectToString(valueToObject(contents, isArray), language));
},
[isArray, language]
);

const dropZoneInputRef = useRef<HTMLInputElement>(null);
const handleUpload = useCallback(() => dropZoneInputRef.current?.click(), []);

const handleDownload = useCallback(() => {
const fileName = name || 'data';
const extension = language === 'json' ? 'json' : 'yaml';
downloadTextFile(
fileName,
objectToString(valueToObject(getValues(name), isArray), language),
extension
);
alertToaster.addAlert({ variant: 'success', title: t('File downloaded'), timeout: true });
}, [alertToaster, getValues, isArray, language, name, t]);

const value = getValues(name);
useLayoutEffect(() => {
setDataEditorValue(objectToString(valueToObject(value, isArray), language));
}, [value, getValues, isArray, language, name]);

const required = useRequiredValidationRule(props.label, props.isRequired);

return (
<Controller<TFieldValues, TFieldName>
name={name}
control={control}
shouldUnregister
render={({ field: { name, onChange }, fieldState: { error } }) => {
function handleChange(stringValue: string) {
switch (valueFormat) {
case 'object':
onChange(valueToObject(stringValue, isArray));
return;
default:
onChange(objectToString(valueToObject(stringValue, isArray), valueFormat));
break;
}
}
return (
<PageFormGroup
fieldId={id}
icon={
!disableExpand && <ExpandIcon isExpanded={isExpanded} setExpanded={setExpanded} />
}
label={props.label}
labelHelpTitle={props.labelHelpTitle ?? props.label}
labelHelp={props.labelHelp}
additionalControls={
<DataEditorActions
handleCopy={!disableCopy && handleCopy}
handleUpload={!disableUpload && handleUpload}
handleDownload={!disableDownload && handleDownload}
language={language}
setLanguage={setLanguage}
>
{props.additionalControls}
</DataEditorActions>
}
helperText={props.helperText}
helperTextInvalid={!(validate && isValidating) && error?.message?.split('\n')}
isRequired={props.isRequired}
>
{isExpanded && (
<DropZone
onDrop={onDrop}
isDisabled={isSubmitting || props.isReadOnly}
inputRef={dropZoneInputRef}
>
<DataEditor
data-cy={id}
id={id}
name={name}
language={language}
value={dataEditorValue}
onChange={handleChange}
setError={(error) => {
if (!error) clearErrors(name);
else setError(name, { message: error });
}}
isReadOnly={props.isReadOnly || isSubmitting}
className={
props.isReadOnly ? `pf-v5-c-form-control pf-m-disabled` : `pf-v5-c-form-control`
}
/>
</DropZone>
)}
{!isExpanded && <div className="pf-v5-c-form-control" />}
</PageFormGroup>
);
}}
rules={{ required, validate: props.validate }}
/>
);
}