Skip to content

Commit

Permalink
added data connection section (#1000)
Browse files Browse the repository at this point in the history
* added data connection section

added upload for secret

* removed extra PF props

* fixed props

* added data connection form validation

* fixes

* added fix

* linter
  • Loading branch information
Gkrumbach07 committed Mar 24, 2023
1 parent af79b0b commit 32f4555
Show file tree
Hide file tree
Showing 14 changed files with 409 additions and 40 deletions.
84 changes: 77 additions & 7 deletions frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
Stack,
StackItem,
} from '@patternfly/react-core';
import { createNotebook, updateNotebook } from '~/api';
import { StartNotebookData, StorageData, EnvVariable } from '~/pages/projects/types';
import { assembleSecret, createNotebook, createSecret, updateNotebook } from '~/api';
import {
StartNotebookData,
StorageData,
EnvVariable,
DataConnectionData,
} from '~/pages/projects/types';
import { useUser } from '~/redux/selectors';
import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext';
import { AppContext } from '~/app/AppContext';
Expand All @@ -21,17 +26,20 @@ import {
updateConfigMapsAndSecretsForNotebook,
} from './service';
import { checkRequiredFieldsForNotebookStart } from './spawnerUtils';
import { getNotebookDataConnection } from './dataConnection/useNotebookDataConnection';

type SpawnerFooterProps = {
startNotebookData: StartNotebookData;
storageData: StorageData;
envVariables: EnvVariable[];
dataConnection: DataConnectionData;
};

const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
startNotebookData,
storageData,
envVariables,
dataConnection,
}) => {
const [errorMessage, setErrorMessage] = React.useState('');
const {
Expand All @@ -42,6 +50,7 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
const tolerationSettings = notebookController?.notebookTolerationSettings;
const {
notebooks: { data },
dataConnections: { data: existingDataConnections },
refreshAllProjectData,
} = React.useContext(ProjectDetailsContext);
const { notebookName } = useParams();
Expand All @@ -54,8 +63,17 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
const [createInProgress, setCreateInProgress] = React.useState(false);
const isButtonDisabled =
createInProgress ||
!checkRequiredFieldsForNotebookStart(startNotebookData, storageData, envVariables);
!checkRequiredFieldsForNotebookStart(
startNotebookData,
storageData,
envVariables,
dataConnection,
);
const { username } = useUser();
const existingNotebookDataConnection = getNotebookDataConnection(
editNotebook,
existingDataConnections,
);

const afterStart = (name: string, type: 'created' | 'updated') => {
const { gpus, notebookSize, image } = startNotebookData;
Expand All @@ -79,6 +97,26 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
};

const onUpdateNotebook = async () => {
if (dataConnection.type === 'creating') {
const dataAsRecord = dataConnection.creating?.values?.data.reduce<Record<string, string>>(
(acc, { key, value }) => ({ ...acc, [key]: value }),
{},
);
if (dataAsRecord) {
const isSuccess = await createSecret(assembleSecret(projectName, dataAsRecord, 'aws'), {
dryRun: true,
})
.then(() => true)
.catch((e) => {
handleError(e);
return false;
});
if (!isSuccess) {
return;
}
}
}

handleStart();
if (editNotebook) {
const pvcDetails = await replaceRootVolumesForNotebook(
Expand All @@ -90,6 +128,8 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
projectName,
editNotebook,
envVariables,
dataConnection,
existingNotebookDataConnection,
).catch(handleError);

if (!pvcDetails || !envFrom) {
Expand All @@ -110,12 +150,42 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
};

const onCreateNotebook = async () => {
if (dataConnection.type === 'creating') {
const dataAsRecord = dataConnection.creating?.values?.data.reduce<Record<string, string>>(
(acc, { key, value }) => ({ ...acc, [key]: value }),
{},
);
if (dataAsRecord) {
const isSuccess = await createSecret(assembleSecret(projectName, dataAsRecord, 'aws'), {
dryRun: true,
})
.then(() => true)
.catch((e) => {
handleError(e);
return false;
});
if (!isSuccess) {
return;
}
}
}

handleStart();

const newDataConnection =
dataConnection.enabled && dataConnection.type === 'creating' && dataConnection.creating
? [dataConnection.creating]
: [];
const existingDataConnection =
dataConnection.enabled && dataConnection.type === 'existing' && dataConnection.existing
? [dataConnection.existing]
: [];

const pvcDetails = await createPvcDataForNotebook(projectName, storageData).catch(handleError);
const envFrom = await createConfigMapsAndSecretsForNotebook(projectName, envVariables).catch(
handleError,
);
const envFrom = await createConfigMapsAndSecretsForNotebook(projectName, [
...envVariables,
...newDataConnection,
]).catch(handleError);

if (!pvcDetails || !envFrom) {
// Error happened, let the error code handle it
Expand All @@ -127,7 +197,7 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
...startNotebookData,
volumes,
volumeMounts,
envFrom,
envFrom: [...envFrom, ...existingDataConnection],
tolerationSettings,
};

Expand Down
19 changes: 18 additions & 1 deletion frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ import EnvironmentVariables from './environmentVariables/EnvironmentVariables';
import { useStorageDataObject } from './storage/utils';
import { getRootVolumeName, useMergeDefaultPVCName } from './spawnerUtils';
import { useNotebookEnvVariables } from './environmentVariables/useNotebookEnvVariables';
import DataConnectionField from './dataConnection/DataConnectionField';
import { useNotebookDataConnection } from './dataConnection/useNotebookDataConnection';

type SpawnerPageProps = {
existingNotebook?: NotebookKind;
};

const SpawnerPage: React.FC<SpawnerPageProps> = ({ existingNotebook }) => {
const { currentProject } = React.useContext(ProjectDetailsContext);
const { currentProject, dataConnections } = React.useContext(ProjectDetailsContext);
const displayName = getProjectDisplayName(currentProject);

const [nameDesc, setNameDesc] = React.useState<NameDescType>({
Expand All @@ -62,6 +64,10 @@ const SpawnerPage: React.FC<SpawnerPageProps> = ({ existingNotebook }) => {
const [storageDataWithoutDefault, setStorageData] = useStorageDataObject(existingNotebook);
const storageData = useMergeDefaultPVCName(storageDataWithoutDefault, nameDesc.name);
const [envVariables, setEnvVariables] = useNotebookEnvVariables(existingNotebook);
const [dataConnection, setDataConnection] = useNotebookDataConnection(
existingNotebook,
dataConnections.data,
);

const restartNotebooks = useWillNotebooksRestart([existingNotebook?.metadata.name || '']);

Expand Down Expand Up @@ -191,6 +197,16 @@ const SpawnerPage: React.FC<SpawnerPageProps> = ({ existingNotebook }) => {
editStorage={getRootVolumeName(existingNotebook)}
/>
</FormSection>
<FormSection
title={SpawnerPageSectionTitles[SpawnerPageSectionID.DATA_CONNECTIONS]}
id={SpawnerPageSectionID.DATA_CONNECTIONS}
aria-label={SpawnerPageSectionTitles[SpawnerPageSectionID.DATA_CONNECTIONS]}
>
<DataConnectionField
dataConnection={dataConnection}
setDataConnection={(connection) => setDataConnection(connection)}
/>
</FormSection>
</Form>
</GenericSidebar>
</PageSection>
Expand All @@ -215,6 +231,7 @@ const SpawnerPage: React.FC<SpawnerPageProps> = ({ existingNotebook }) => {
}}
storageData={storageData}
envVariables={envVariables}
dataConnection={dataConnection}
/>
</StackItem>
</Stack>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/projects/screens/spawner/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SpawnerPageSectionTitles: SpawnerPageSectionTitlesType = {
[SpawnerPageSectionID.DEPLOYMENT_SIZE]: 'Deployment size',
[SpawnerPageSectionID.ENVIRONMENT_VARIABLES]: 'Environment variables',
[SpawnerPageSectionID.CLUSTER_STORAGE]: 'Cluster storage',
[SpawnerPageSectionID.DATA_CONNECTIONS]: 'Data connections',
};

export const ScrollableSelectorID = 'workbench-spawner-page';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react';
import { Checkbox, FormGroup, Radio, Stack, StackItem } from '@patternfly/react-core';
import {
DataConnectionData,
EnvironmentVariableType,
SecretCategory,
} from '~/pages/projects/types';
import AWSField from '~/pages/projects/dataConnections/AWSField';
import { EMPTY_AWS_SECRET_DATA } from '~/pages/projects/dataConnections/const';
import ExistingDataConnectionField from './ExistingDataConnectionField';

type DataConnectionFieldProps = {
dataConnection: DataConnectionData;
setDataConnection: (dataConnection: DataConnectionData) => void;
};
const DataConnectionField: React.FC<DataConnectionFieldProps> = ({
dataConnection,
setDataConnection,
}) => (
<FormGroup fieldId="cluster-storage" role="radiogroup">
<Stack hasGutter>
<StackItem>
<Checkbox
className="checkbox-radio-fix-body-width"
name="enable-data-connection-checkbox"
id="enable-data-connection-checkbox"
label="Use a data connection"
isChecked={dataConnection.enabled}
onChange={() =>
setDataConnection({ ...dataConnection, enabled: !dataConnection.enabled })
}
/>
</StackItem>
{dataConnection.enabled && (
<>
<StackItem>
<Radio
className="checkbox-radio-fix-body-width"
name="new-data-connection-radio"
id="new-data-connection-radio"
label="Create new data connection"
isChecked={dataConnection.type === 'creating'}
onChange={() => setDataConnection({ ...dataConnection, type: 'creating' })}
body={
dataConnection.type === 'creating' && (
<AWSField
values={dataConnection.creating?.values?.data ?? EMPTY_AWS_SECRET_DATA}
onUpdate={(newEnvData) => {
setDataConnection({
...dataConnection,
creating: {
type: EnvironmentVariableType.SECRET,
values: { category: SecretCategory.AWS, data: newEnvData },
},
});
}}
/>
)
}
/>
</StackItem>
<StackItem>
<Radio
className="checkbox-radio-fix-body-width"
name="existing-data-connection-type-radio"
id="existing-data-connection-type-radio"
label="Use existing data connection"
isChecked={dataConnection.type === 'existing'}
onChange={() => setDataConnection({ ...dataConnection, type: 'existing' })}
body={
dataConnection.type === 'existing' && (
<ExistingDataConnectionField
fieldId="select-existing-data-connection"
selectedDataConnection={dataConnection?.existing?.secretRef.name}
setDataConnection={(name) =>
setDataConnection({
...dataConnection,
existing: { secretRef: { name: name ?? '' } },
})
}
/>
)
}
/>
</StackItem>
</>
)}
</Stack>
</FormGroup>
);

export default DataConnectionField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import { Alert, FormGroup, Select, SelectOption } from '@patternfly/react-core';
import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext';
import { getDataConnectionDisplayName } from '~/pages/projects/screens/detail/data-connections/utils';

type ExistingDataConnectionFieldProps = {
fieldId: string;
selectedDataConnection?: string;
setDataConnection: (name?: string) => void;
};

const ExistingDataConnectionField: React.FC<ExistingDataConnectionFieldProps> = ({
fieldId,
selectedDataConnection,
setDataConnection,
}) => {
const [isOpen, setOpen] = React.useState(false);
const {
dataConnections: { data: connections, loaded, error },
} = React.useContext(ProjectDetailsContext);

if (error) {
return (
<Alert title="Error loading data connections" variant="danger">
{error.message}
</Alert>
);
}

const empty = connections.length === 0;
let placeholderText: string;
if (!loaded) {
placeholderText = 'Loading data connections...';
} else if (empty) {
placeholderText = 'No existing data connections available';
} else {
placeholderText = 'Select a data connection';
}

return (
<FormGroup isRequired label="Data connection" fieldId={fieldId}>
<Select
removeFindDomNode
variant="typeahead"
selections={selectedDataConnection}
isOpen={isOpen}
onClear={() => {
setDataConnection(undefined);
setOpen(false);
}}
isDisabled={empty}
onSelect={(e, selection) => {
if (typeof selection === 'string') {
setDataConnection(selection);
setOpen(false);
}
}}
onToggle={setOpen}
placeholderText={placeholderText}
direction="up"
menuAppendTo="parent"
>
{connections.map((connection) => (
<SelectOption key={connection.data.metadata.name} value={connection.data.metadata.name}>
{getDataConnectionDisplayName(connection)}
</SelectOption>
))}
</Select>
</FormGroup>
);
};

export default ExistingDataConnectionField;

0 comments on commit 32f4555

Please sign in to comment.