Skip to content

Commit

Permalink
Allow edit of existing project connections
Browse files Browse the repository at this point in the history
  • Loading branch information
emilys314 committed Sep 25, 2024
1 parent 7415077 commit 8008cf0
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,36 @@ describe('Connections', () => {

cy.wait('@createConnection');
});

it('Edit a connection', () => {
initIntercepts();
cy.interceptOdh('GET /api/connection-types', [
mockConnectionTypeConfigMap({
name: 'postgres',
fields: [
{
name: 'field A',
type: ConnectionTypeFieldType.ShortText,
envVar: 'field_env',
properties: {},
},
],
}),
]);
cy.interceptK8s(
'PUT',
SecretModel,
mockSecretK8sResource({
name: 'test2',
}),
).as('editConnection');

projectDetails.visitSection('test-project', 'connections');

connectionsPage.getConnectionRow('test2').findKebabAction('Edit').click();
cy.findByTestId(['field_env']).fill('new data');
cy.findByTestId('modal-submit-button').click();

cy.wait('@editConnection');
});
});
10 changes: 6 additions & 4 deletions frontend/src/concepts/connectionTypes/ConnectionTypeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Props = Pick<
connectionValues?: {
[key: string]: ConnectionTypeValueType;
};
disableTypeSelection?: boolean;
};

const ConnectionTypeForm: React.FC<Props> = ({
Expand All @@ -42,6 +43,7 @@ const ConnectionTypeForm: React.FC<Props> = ({
connectionValues,
onChange,
onValidate,
disableTypeSelection,
}) => {
const options: TypeaheadSelectOption[] = React.useMemo(() => {
if (isPreview && connectionType?.metadata.annotations?.['openshift.io/display-name']) {
Expand Down Expand Up @@ -73,7 +75,7 @@ const ConnectionTypeForm: React.FC<Props> = ({
onSelect={(_, selection) =>
setConnectionType?.(connectionTypes?.find((c) => c.metadata.name === selection))
}
isDisabled={isPreview || connectionTypes?.length === 1}
isDisabled={isPreview || disableTypeSelection}
placeholder={
isPreview && !connectionType?.metadata.annotations?.['openshift.io/display-name']
? 'Unspecified'
Expand All @@ -90,7 +92,7 @@ const ConnectionTypeForm: React.FC<Props> = ({
)}
</FormGroup>
{(isPreview || connectionType?.metadata.name) && (
<FormSection title="Connection details" style={{ marginTop: 0 }}>
<FormSection title="Connection details">
<K8sNameDescriptionField
dataTestId="connection-name-desc"
nameLabel="Connection name"
Expand All @@ -102,7 +104,7 @@ const ConnectionTypeForm: React.FC<Props> = ({
k8sName: {
value: '',
state: {
immutable: true,
immutable: false,
invalidCharacters: false,
invalidLength: false,
maxLength: 0,
Expand All @@ -111,7 +113,7 @@ const ConnectionTypeForm: React.FC<Props> = ({
},
}
}
onDataChange={setConnectionNameDesc}
onDataChange={setConnectionNameDesc ?? (() => undefined)} // onDataChange needs to be truthy to show resource name
/>
<ConnectionTypeFormFields
fields={connectionType?.data?.fields}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ConnectionTypeField,
ConnectionTypeFieldType,
ConnectionTypeValueType,
isConnectionTypeDataField,
SectionField,
} from '~/concepts/connectionTypes/types';

Expand Down Expand Up @@ -47,6 +48,22 @@ const ConnectionTypeFormFields: React.FC<Props> = ({
[fields],
);

const unmatchedValues: ConnectionTypeDataField[] = React.useMemo(() => {
const unmatched: ConnectionTypeDataField[] = [];
for (const key in connectionValues) {
const matching = fields?.find((f) => isConnectionTypeDataField(f) && f.envVar === key);
if (!matching) {
unmatched.push({
type: ConnectionTypeFieldType.ShortText,
envVar: key,
name: key,
properties: {},
});
}
}
return unmatched;
}, [connectionValues, fields]);

const renderDataFields = (dataFields: ConnectionTypeDataField[]) =>
dataFields.map((field, i) => (
<DataFormFieldGroup key={i} field={field}>
Expand Down Expand Up @@ -74,6 +91,7 @@ const ConnectionTypeFormFields: React.FC<Props> = ({
<React.Fragment key={i}>{renderDataFields(fieldGroup.fields)}</React.Fragment>
),
)}
{unmatchedValues.length > 0 && renderDataFields(unmatchedValues)}
</>
);
};
Expand Down
40 changes: 39 additions & 1 deletion frontend/src/concepts/connectionTypes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ export const assembleConnectionSecret = (
},
): Connection => {
const connectionValuesAsStrings = Object.fromEntries(
Object.entries(values).map(([key, value]) => [key, String(value)]),
Object.entries(values).map(([key, value]) => {
if (Array.isArray(value)) {
return [key, JSON.stringify(value)]; // multi select
}
return [key, String(value)];
}),
);
return {
apiVersion: 'v1',
Expand All @@ -158,3 +163,36 @@ export const assembleConnectionSecret = (
stringData: connectionValuesAsStrings,
};
};

export const parseConnectionSecretValues = (
connection: Connection,
connectionType?: ConnectionTypeConfigMapObj,
): { [key: string]: ConnectionTypeValueType } => {
const response: { [key: string]: ConnectionTypeValueType } = {};

for (const [key, value] of Object.entries(connection.data ?? {})) {
const decodedString = window.atob(value);
const matchingField = connectionType?.data?.fields?.find(
(f) => isConnectionTypeDataField(f) && f.envVar === key,
);

if (matchingField?.type === ConnectionTypeFieldType.Boolean) {
response[key] = decodedString === 'true';
} else if (matchingField?.type === ConnectionTypeFieldType.Numeric) {
response[key] = Number(decodedString);
} else if (
matchingField?.type === ConnectionTypeFieldType.Dropdown &&
matchingField.properties.variant === 'multi'
) {
try {
response[key] = JSON.parse(decodedString);
} catch {
response[key] = decodedString;
}
} else {
response[key] = decodedString;
}
}

return response;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconBut
import { ProjectObjectType, typedEmptyImage } from '~/concepts/design/utils';
import { Connection } from '~/concepts/connectionTypes/types';
import { useWatchConnectionTypes } from '~/utilities/useWatchConnectionTypes';
import { createSecret } from '~/api';
import { createSecret, replaceSecret } from '~/api';
import ConnectionsTable from './ConnectionsTable';
import { ManageConnectionModal } from './ManageConnectionsModal';

Expand All @@ -26,6 +26,7 @@ const ConnectionsList: React.FC = () => {

const [manageConnectionModal, setManageConnectionModal] = React.useState<{
connection?: Connection;
isEdit?: boolean;
}>();

return (
Expand Down Expand Up @@ -75,6 +76,9 @@ const ConnectionsList: React.FC = () => {
connections={connections}
connectionTypes={connectionTypes}
refreshConnections={refreshConnections}
setManageConnectionModal={(modalConnection?: Connection) =>
setManageConnectionModal({ connection: modalConnection, isEdit: true })
}
/>
{manageConnectionModal && (
<ManageConnectionModal
Expand All @@ -87,7 +91,10 @@ const ConnectionsList: React.FC = () => {
refreshConnections();
}
}}
onSubmit={(connection: Connection) => createSecret(connection)}
onSubmit={(connection: Connection) =>
manageConnectionModal.isEdit ? replaceSecret(connection) : createSecret(connection)
}
isEdit={manageConnectionModal.isEdit}
/>
)}
</DetailsSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ type ConnectionsTableProps = {
connections: Connection[];
connectionTypes?: ConnectionTypeConfigMapObj[];
refreshConnections: () => void;
setManageConnectionModal: (connection: Connection) => void;
};

const ConnectionsTable: React.FC<ConnectionsTableProps> = ({
connections,
connectionTypes,
refreshConnections,
setManageConnectionModal,
}) => {
const [deleteConnection, setDeleteConnection] = React.useState<Connection>();

Expand All @@ -30,7 +32,7 @@ const ConnectionsTable: React.FC<ConnectionsTableProps> = ({
key={connection.metadata.name}
obj={connection}
connectionTypes={connectionTypes}
onEditConnection={() => undefined}
onEditConnection={() => setManageConnectionModal(connection)}
onDeleteConnection={() => setDeleteConnection(connection)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Modal } from '@patternfly/react-core';
import { Alert, Modal } from '@patternfly/react-core';
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter';
import ConnectionTypeForm from '~/concepts/connectionTypes/ConnectionTypeForm';
import {
Expand All @@ -11,14 +11,20 @@ import {
} from '~/concepts/connectionTypes/types';
import { ProjectKind, SecretKind } from '~/k8sTypes';
import { useK8sNameDescriptionFieldData } from '~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField';
import { assembleConnectionSecret, getDefaultValues } from '~/concepts/connectionTypes/utils';
import {
assembleConnectionSecret,
getDefaultValues,
parseConnectionSecretValues,
} from '~/concepts/connectionTypes/utils';
import { K8sNameDescriptionFieldData } from '~/concepts/k8s/K8sNameDescriptionField/types';

type Props = {
connection?: Connection;
connectionTypes: ConnectionTypeConfigMapObj[];
project: ProjectKind;
onClose: (submitted?: boolean) => void;
onSubmit: (connection: Connection) => Promise<SecretKind>;
isEdit?: boolean;
};

export const ManageConnectionModal: React.FC<Props> = ({
Expand All @@ -27,9 +33,11 @@ export const ManageConnectionModal: React.FC<Props> = ({
project,
onClose,
onSubmit,
isEdit = false,
}) => {
const [error, setError] = React.useState<Error>();
const [isSaving, setIsSaving] = React.useState(false);
const [isModified, setIsModified] = React.useState(false);

const enabledConnectionTypes = React.useMemo(
() =>
Expand All @@ -39,13 +47,26 @@ export const ManageConnectionModal: React.FC<Props> = ({

const [selectedConnectionType, setSelectedConnectionType] = React.useState<
ConnectionTypeConfigMapObj | undefined
>(enabledConnectionTypes.length === 1 ? enabledConnectionTypes[0] : undefined);
const { data: nameDescData, onDataChange: setNameDescData } = useK8sNameDescriptionFieldData();
>(() => {
if (isEdit && connection) {
return connectionTypes.find(
(t) =>
t.metadata.name === connection.metadata.annotations['opendatahub.io/connection-type'],
);
}
if (enabledConnectionTypes.length === 1) {
return enabledConnectionTypes[0];
}
return undefined;
});
const { data: nameDescData, onDataChange: setNameDescData } = useK8sNameDescriptionFieldData({
initialData: connection,
});
const [connectionValues, setConnectionValues] = React.useState<{
[key: string]: ConnectionTypeValueType;
}>(() => {
if (connection?.data) {
return connection.data;
return parseConnectionSecretValues(connection, selectedConnectionType);
}
if (enabledConnectionTypes.length === 1) {
return getDefaultValues(enabledConnectionTypes[0]);
Expand Down Expand Up @@ -101,15 +122,15 @@ export const ManageConnectionModal: React.FC<Props> = ({

return (
<Modal
title="Add Connection"
title={isEdit ? 'Edit connection' : 'Add connection'}
isOpen
onClose={() => {
onClose();
}}
variant="medium"
footer={
<DashboardModalFooter
submitLabel="Create"
submitLabel={isEdit ? 'Save' : 'Create'}
onCancel={onClose}
onSubmit={() => {
setIsSaving(true);
Expand Down Expand Up @@ -139,25 +160,50 @@ export const ManageConnectionModal: React.FC<Props> = ({
});
}}
error={error}
isSubmitDisabled={!isFormValid}
isSubmitDisabled={!isFormValid || !isModified}
isSubmitLoading={isSaving}
alertTitle=""
/>
}
>
{isEdit && (
<Alert
style={{ marginBottom: 32 }}
variant="warning"
isInline
title="Dependent resources require further action"
>
Connection changes are not applied to dependent resources until those resources are
restarted, redeployed, or otherwise regenerated.
</Alert>
)}
<ConnectionTypeForm
connectionTypes={enabledConnectionTypes}
connectionType={selectedConnectionType}
setConnectionType={changeSelectionType}
setConnectionType={(obj?: ConnectionTypeConfigMapObj) => {
if (!isModified) {
setIsModified(true);
}
changeSelectionType(obj);
}}
connectionNameDesc={nameDescData}
setConnectionNameDesc={setNameDescData}
setConnectionNameDesc={(key: keyof K8sNameDescriptionFieldData, value: string) => {
if (!isModified) {
setIsModified(true);
}
setNameDescData(key, value);
}}
connectionValues={connectionValues}
onChange={(field, value) =>
setConnectionValues((prev) => ({ ...prev, [field.envVar]: value }))
}
onChange={(field, value) => {
if (!isModified) {
setIsModified(true);
}
setConnectionValues((prev) => ({ ...prev, [field.envVar]: value }));
}}
onValidate={(field, isValid) =>
setValidations((prev) => ({ ...prev, [field.envVar]: isValid }))
}
disableTypeSelection={isEdit || enabledConnectionTypes.length === 1}
/>
</Modal>
);
Expand Down
Loading

0 comments on commit 8008cf0

Please sign in to comment.