diff --git a/client/src/features/dataConnectorsV2/components/useDataConnectorConfiguration.hook.ts b/client/src/features/dataConnectorsV2/components/useDataConnectorConfiguration.hook.ts index d89cfbefdf..fffb537cd0 100644 --- a/client/src/features/dataConnectorsV2/components/useDataConnectorConfiguration.hook.ts +++ b/client/src/features/dataConnectorsV2/components/useDataConnectorConfiguration.hook.ts @@ -85,14 +85,17 @@ export default function useDataConnectorConfiguration({ const savedCredentialFields = dataConnectorSecrets ? dataConnectorSecrets[dataConnector.id]?.map((s) => s.name) : []; - return { + const result: SessionStartDataConnectorConfiguration = { active: true, dataConnector, - sensitiveFieldDefinitions, - sensitiveFieldValues, saveCredentials: false, savedCredentialFields, + sensitiveFieldDefinitions, + sensitiveFieldValues, + skip: false, + touched: false, }; + return result; }), [dataConnectors, dataConnectorSecrets] ); diff --git a/client/src/features/project/utils/projectCloudStorage.utils.test.ts b/client/src/features/project/utils/projectCloudStorage.utils.test.ts index 1ed7d7e778..66ac98ff63 100644 --- a/client/src/features/project/utils/projectCloudStorage.utils.test.ts +++ b/client/src/features/project/utils/projectCloudStorage.utils.test.ts @@ -1,14 +1,16 @@ import type { SessionStartDataConnectorConfiguration } from "../../sessionsV2/startSessionOptionsV2.types"; import type { CloudStorageSchema } from "../components/cloudStorage/projectCloudStorage.types"; import { + dataConnectorsOverrideFromConfig, getSchemaOptions, - storageDefinitionFromConfig, } from "./projectCloudStorage.utils"; describe("storageDefinitionFromConfig", () => { it("should return the correct storage definition", () => { const config: SessionStartDataConnectorConfiguration = { active: true, + skip: false, + touched: true, dataConnector: { id: "ULID-1", etag: "foo", @@ -79,21 +81,19 @@ describe("storageDefinitionFromConfig", () => { saveCredentials: false, savedCredentialFields: [], }; - const result = storageDefinitionFromConfig(config); - expect(result).toEqual({ - configuration: { - type: "s3", - provider: "AWS", - access_key_id: "access key", - secret_access_key: "secret key", + const result = dataConnectorsOverrideFromConfig(config); + expect(result).toEqual([ + { + configuration: { + type: "s3", + provider: "AWS", + access_key_id: "access key", + secret_access_key: "secret key", + }, + data_connector_id: "ULID-1", + skip: false, }, - name: "example-storage", - readonly: true, - source_path: "bucket/my-source", - storage_id: "ULID-1", - storage_type: "s3", - target_path: "external_storage/aws", - }); + ]); }); it("should return the correct schema options", () => { diff --git a/client/src/features/project/utils/projectCloudStorage.utils.ts b/client/src/features/project/utils/projectCloudStorage.utils.ts index e165fc58a7..eec62fa515 100644 --- a/client/src/features/project/utils/projectCloudStorage.utils.ts +++ b/client/src/features/project/utils/projectCloudStorage.utils.ts @@ -16,12 +16,13 @@ * limitations under the License. */ +import { type SessionDataConnectorOverride } from "~/features/sessionsV2/api/sessionsV2.api"; import type { RCloneConfig, RCloneOption, } from "../../dataConnectorsV2/api/data-connectors.api"; import { hasSchemaAccessMode } from "../../dataConnectorsV2/components/dataConnector.utils"; -import type { SessionCloudStorageV2 } from "../../sessionsV2/sessionsV2.types"; +import type { SessionStartDataConnectorConfiguration } from "../../sessionsV2/startSessionOptionsV2.types"; import type { CloudStorageGet } from "../components/cloudStorage/api/projectCloudStorage.api"; import { CLOUD_OPTIONS_OVERRIDE, @@ -35,17 +36,15 @@ import { STORAGES_WITH_ACCESS_MODE, } from "../components/cloudStorage/projectCloudStorage.constants"; import type { + CloudStorage, CloudStorageCredential, CloudStorageDetails, CloudStorageOptionTypes, - CloudStorageSchemaOption, CloudStorageProvider, CloudStorageSchema, - CloudStorage, + CloudStorageSchemaOption, } from "../components/cloudStorage/projectCloudStorage.types"; -import { SessionStartDataConnectorConfiguration } from "../../sessionsV2/startSessionOptionsV2.types"; - const LAST_POSITION = 1000; export interface CloudStorageOptions extends RCloneOption { @@ -324,8 +323,12 @@ export function findSensitive( export function storageDefinitionAfterSavingCredentialsFromConfig( cs: SessionStartDataConnectorConfiguration -) { - const newCs = { ...cs, saveCredentials: false }; +): SessionStartDataConnectorConfiguration { + const newCs: SessionStartDataConnectorConfiguration = { + ...cs, + saveCredentials: false, + touched: false, + }; const newStorage = { ...newCs.dataConnector.storage }; // The following two lines remove the sensitive fields from the storage configuration, // which should be ok, but isn't; so keep in the sensitive fields. @@ -339,27 +342,27 @@ export function storageDefinitionAfterSavingCredentialsFromConfig( return newCs; } -export function storageDefinitionFromConfig( +export function dataConnectorsOverrideFromConfig( config: SessionStartDataConnectorConfiguration -): SessionCloudStorageV2 { - const storageDefinition = config.dataConnector.storage; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { sensitive_fields, ...s } = config.dataConnector.storage; - const newStorageDefinition = { - ...s, - name: config.dataConnector.slug, - storage_id: config.dataConnector.id, - }; - newStorageDefinition.configuration = { ...storageDefinition.configuration }; - const sensitiveFieldValues = config.sensitiveFieldValues; - Object.entries(sensitiveFieldValues).forEach(([name, value]) => { +): SessionDataConnectorOverride[] { + if (!config.skip && !config.touched) { + return []; + } + + const configuration = { ...config.dataConnector.storage.configuration }; + Object.entries(config.sensitiveFieldValues).forEach(([name, value]) => { if (value != null && value !== "") { - newStorageDefinition.configuration[name] = value; + configuration[name] = value; } else { - delete newStorageDefinition.configuration[name]; + delete configuration[name]; } }); - return newStorageDefinition; + const override: SessionDataConnectorOverride = { + skip: config.skip, + data_connector_id: config.dataConnector.id, + configuration, + }; + return [override]; } function overrideOptions( diff --git a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx index 6bb080bd92..954f047314 100644 --- a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx +++ b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx @@ -60,7 +60,7 @@ const CONTEXT_STRINGS = { dataCy: "session-data-connector-credentials-modal", header: "Session Storage Credentials", testError: - "The data connector could not be mounted. Please retry with different credentials, or skip the test. If you skip, the data connector will still try to mount, using the provided credentials, at session launch time.", + "The data connector could not be mounted. Please retry with different credentials, or skip the data connector. If you skip, the data connector will not be mounted in the session.", }, storage: { continueButton: "Test and Save", @@ -222,6 +222,8 @@ export default function DataConnectorSecretsModal({ newCloudStorageConfigs[index] = { ...dataConnectorConfigs[index], active: false, + saveCredentials: false, + skip: true, }; setDataConnectorConfigs(newCloudStorageConfigs); onNext(newCloudStorageConfigs); @@ -232,7 +234,10 @@ export default function DataConnectorSecretsModal({ if (dataConnectorConfigs == null || dataConnectorConfigs.length < 1) return; - const config = { ...dataConnectorConfigs[index] }; + const config: DataConnectorConfiguration = { + ...dataConnectorConfigs[index], + touched: true, + }; const sensitiveFieldValues = { ...config.sensitiveFieldValues }; const { saveCredentials } = options; if (saveCredentials === true || saveCredentials === false) { @@ -348,12 +353,7 @@ function CredentialsButtons({ Cancel - {context === "session" && ( - - )} + {context === "session" && } {context === "storage" && ( ) { +}: Pick) { const skipButtonRef = useRef(null); return ( <> @@ -626,12 +626,7 @@ function SkipConnectionTestButton({ - Skip the connection test. At session launch, the storage will try to - mount - {validationResult.isError - ? " using the provided credentials" - : " without any credentials"} - . + Skip the data connector. It will not be mounted in the session. ); diff --git a/client/src/features/sessionsV2/SessionStartPage.tsx b/client/src/features/sessionsV2/SessionStartPage.tsx index 809a19d38e..1c88b1ee02 100644 --- a/client/src/features/sessionsV2/SessionStartPage.tsx +++ b/client/src/features/sessionsV2/SessionStartPage.tsx @@ -25,6 +25,7 @@ import { useParams, useSearchParams, } from "react-router"; + import PageLoader from "../../components/PageLoader"; import { RtkErrorAlert, @@ -44,8 +45,8 @@ import { usePatchDataConnectorsByDataConnectorIdSecretsMutation } from "../dataC import type { DataConnectorConfiguration } from "../dataConnectorsV2/components/useDataConnectorConfiguration.hook"; import { resetFavicon, setFavicon } from "../display"; import { + dataConnectorsOverrideFromConfig, storageDefinitionAfterSavingCredentialsFromConfig, - storageDefinitionFromConfig, } from "../project/utils/projectCloudStorage.utils"; import type { Project } from "../projectsV2/api/projectV2.api"; import { useGetNamespacesByNamespaceProjectsAndSlugQuery } from "../projectsV2/api/projectV2.enhanced-api"; @@ -55,19 +56,22 @@ import SessionImageModal from "./SessionImageModal"; import SessionSecretsModal from "./SessionSecretsModal"; import type { SessionLauncher } from "./api/sessionLaunchersV2.api"; import { useGetProjectsByProjectIdSessionLaunchersQuery as useGetProjectSessionLaunchersQuery } from "./api/sessionLaunchersV2.api"; -import { usePostSessionsMutation as useLaunchSessionMutation } from "./api/sessionsV2.api"; +import { + usePostSessionsMutation as useLaunchSessionMutation, + type SessionPostRequest, +} from "./api/sessionsV2.api"; import { SelectResourceClassModal } from "./components/SessionModals/SelectResourceClass"; import { CUSTOM_LAUNCH_SEARCH_PARAM } from "./session.constants"; import { validateEnvVariableName } from "./session.utils"; import startSessionOptionsV2Slice from "./startSessionOptionsV2.slice"; -import { +import type { SessionStartDataConnectorConfiguration, StartSessionOptionsV2, } from "./startSessionOptionsV2.types"; import useSessionLaunchState from "./useSessionLaunchState.hook"; interface SaveCloudStorageProps - extends Omit { + extends Omit { startSessionOptionsV2: StartSessionOptionsV2; } @@ -81,8 +85,8 @@ function SaveCloudStorage({ usePatchDataConnectorsByDataConnectorIdSecretsMutation(); const credentialsToSave = useMemo(() => { - return startSessionOptionsV2.cloudStorage - ? startSessionOptionsV2.cloudStorage + return startSessionOptionsV2.dataConnectors + ? startSessionOptionsV2.dataConnectors .filter(shouldCloudStorageSaveCredentials) .map((cs) => ({ storageName: cs.dataConnector.name, @@ -90,7 +94,7 @@ function SaveCloudStorage({ secrets: cs.sensitiveFieldValues, })) : []; - }, [startSessionOptionsV2.cloudStorage]); + }, [startSessionOptionsV2.dataConnectors]); const [results, setResults] = useState( credentialsToSave.map(() => StatusStepProgressBar.WAITING) @@ -153,15 +157,19 @@ function SaveCloudStorage({ }, [index, saveCredentialsResult]); useEffect(() => { - if (saveCredentialsResult.isLoading || !startSessionOptionsV2.cloudStorage) + if ( + saveCredentialsResult.isLoading || + !startSessionOptionsV2.dataConnectors + ) { return; + } if (index >= credentialsToSave.length) { - const cloudStorageConfigs = startSessionOptionsV2.cloudStorage?.map( + const cloudStorageConfigs = startSessionOptionsV2.dataConnectors?.map( (cs) => storageDefinitionAfterSavingCredentialsFromConfig(cs) ); if (cloudStorageConfigs) dispatch( - startSessionOptionsV2Slice.actions.setCloudStorage( + startSessionOptionsV2Slice.actions.setDataConnectorsOverrides( cloudStorageConfigs ) ); @@ -171,7 +179,7 @@ function SaveCloudStorage({ credentialsToSave, index, saveCredentialsResult, - startSessionOptionsV2.cloudStorage, + startSessionOptionsV2.dataConnectors, ]); return ( @@ -202,13 +210,13 @@ function SessionStarting({ launcher, project }: StartSessionFromLauncherProps) { ] = useLaunchSessionMutation(); const launcherToStart = useMemo(() => { - return { + const request: SessionPostRequest = { launcher_id: launcher.id, disk_storage: startSessionOptionsV2.storage, resource_class_id: startSessionOptionsV2.sessionClass, - cloudstorage: startSessionOptionsV2.cloudStorage - ?.filter(({ active }) => active) - .map((cs) => storageDefinitionFromConfig(cs)), + data_connectors_overrides: startSessionOptionsV2.dataConnectors?.flatMap( + dataConnectorsOverrideFromConfig + ), env_variable_overrides: Array.from(searchParams) .filter(([name]) => validateEnvVariableName(name) === true) .map(([name, value]) => ({ @@ -216,11 +224,12 @@ function SessionStarting({ launcher, project }: StartSessionFromLauncherProps) { value, })), }; + return request; }, [ launcher.id, startSessionOptionsV2.storage, startSessionOptionsV2.sessionClass, - startSessionOptionsV2.cloudStorage, + startSessionOptionsV2.dataConnectors, searchParams, ]); @@ -298,7 +307,10 @@ function SessionStarting({ launcher, project }: StartSessionFromLauncherProps) { function doesCloudStorageNeedCredentials( config: SessionStartDataConnectorConfiguration ) { - if (config.active === false) return false; + if (!config.active || config.skip) { + return false; + } + const sensitiveFields = Object.keys(config.sensitiveFieldValues); const credentialFieldDict = config.savedCredentialFields ? Object.fromEntries( @@ -308,8 +320,9 @@ function doesCloudStorageNeedCredentials( ]) ) : {}; - if (sensitiveFields.every((key) => credentialFieldDict[key] != null)) + if (sensitiveFields.every((key) => credentialFieldDict[key] != null)) { return false; + } return Object.values(config.sensitiveFieldValues).some( (value) => value === "" ); @@ -322,17 +335,14 @@ function shouldCloudStorageSaveCredentials( } interface StartSessionWithCloudStorageModalProps - extends Omit { - cloudStorageConfigs: Omit< - SessionStartDataConnectorConfiguration, - "sensitiveFields" - >[]; + extends StartSessionFromLauncherProps { + dataConnectors: SessionStartDataConnectorConfiguration[]; } function StartSessionWithCloudStorageModal({ + dataConnectors, launcher, project, - cloudStorageConfigs, }: StartSessionWithCloudStorageModalProps) { const [showDataConnectorSecretsModal, setShowDataConnectorSecretsModal] = useState(false); @@ -340,18 +350,18 @@ function StartSessionWithCloudStorageModal({ const configsWithCredentials = useMemo( () => - cloudStorageConfigs.filter( + dataConnectors.filter( (config) => !doesCloudStorageNeedCredentials(config) ), - [cloudStorageConfigs] + [dataConnectors] ); const configsNeedingCredentials = useMemo( () => - cloudStorageConfigs.filter((config) => + dataConnectors.filter((config) => doesCloudStorageNeedCredentials(config) ), - [cloudStorageConfigs] + [dataConnectors] ); useEffect(() => { @@ -368,7 +378,9 @@ function StartSessionWithCloudStorageModal({ ...changedCloudStorageConfigs, ]; dispatch( - startSessionOptionsV2Slice.actions.setCloudStorage(cloudStorageConfigs) + startSessionOptionsV2Slice.actions.setDataConnectorsOverrides( + cloudStorageConfigs + ) ); }, [dispatch, configsWithCredentials] @@ -456,18 +468,18 @@ function StartSessionFromLauncher({ isCustomLaunch: hasCustomQuery, }); - const needsCredentials = startSessionOptionsV2.cloudStorage?.some( + const needsCredentials = startSessionOptionsV2.dataConnectors?.some( doesCloudStorageNeedCredentials ); - const shouldSaveCredentials = startSessionOptionsV2.cloudStorage?.some( + const shouldSaveCredentials = startSessionOptionsV2.dataConnectors?.some( shouldCloudStorageSaveCredentials ); const allDataFetched = containerImage && startSessionOptionsV2.sessionClass !== 0 && - startSessionOptionsV2.cloudStorage != null && + startSessionOptionsV2.dataConnectors != null && !isFetchingOrLoadingStorages && !isFetchingSessionSecrets && !isLoadingSessionImage; @@ -493,28 +505,31 @@ function StartSessionFromLauncher({ if ( allDataFetched && !needsCredentials && - startSessionOptionsV2.cloudStorage && + startSessionOptionsV2.dataConnectors && shouldSaveCredentials - ) - setShowSaveCredentials(shouldSaveCredentials); - else setShowSaveCredentials(false); + ) { + setShowSaveCredentials(true); + } else { + setShowSaveCredentials(false); + } if ( allDataFetched && !needsCredentials && - startSessionOptionsV2.cloudStorage && + startSessionOptionsV2.dataConnectors && !shouldSaveCredentials && startSessionOptionsV2.userSecretsReady && startSessionOptionsV2.imageReady && !sessionStarted - ) + ) { setSessionStarted(true); + } }, [ allDataFetched, needsCredentials, sessionStarted, shouldSaveCredentials, - startSessionOptionsV2.cloudStorage, + startSessionOptionsV2.dataConnectors, startSessionOptionsV2.imageReady, startSessionOptionsV2.userSecretsReady, ]); @@ -569,11 +584,11 @@ function StartSessionFromLauncher({ if ( allDataFetched && needsCredentials && - startSessionOptionsV2.cloudStorage + startSessionOptionsV2.dataConnectors ) { return ( diff --git a/client/src/features/sessionsV2/api/sessionsV2.generated-api.ts b/client/src/features/sessionsV2/api/sessionsV2.generated-api.ts index c0884f3f88..a418255df0 100644 --- a/client/src/features/sessionsV2/api/sessionsV2.generated-api.ts +++ b/client/src/features/sessionsV2/api/sessionsV2.generated-api.ts @@ -135,7 +135,7 @@ const injectedRtkApi = api.injectEndpoints({ }); export { injectedRtkApi as sessionsV2GeneratedApi }; export type GetNotebooksImagesApiResponse = - /** status 200 undefined */ ImageCheckResponse; + /** status 200 The image check has completed successfully */ ImageCheckResponse; export type GetNotebooksImagesApiArg = { /** The Docker image URL (tag included) that should be fetched. */ imageUrl: string; @@ -438,16 +438,22 @@ export type SessionResponse = { launcher_id: Ulid; resource_class_id: number; }; -export type SessionCloudStoragePost = { - configuration?: { - [key: string]: any; - }; - readonly?: boolean; - source_path?: string; - target_path?: string; - storage_id: Ulid & any; -}; -export type SessionCloudStoragePostList = SessionCloudStoragePost[]; +export type RCloneConfig = { + [key: string]: number | (string | null) | boolean | object; +}; +export type SourcePath = string; +export type TargetPath = string; +export type StorageReadOnly = boolean; +export type SessionDataConnectorOverride = { + /** The corresponding data connector will not be mounted if `skip` is set to `true`. */ + skip?: boolean; + data_connector_id: Ulid & any; + configuration?: RCloneConfig; + source_path?: SourcePath; + target_path?: TargetPath; + readonly?: StorageReadOnly; +}; +export type SessionDataConnectorsOverrideList = SessionDataConnectorOverride[]; export type EnvVarOverride = { name: string; value: string; @@ -458,7 +464,7 @@ export type SessionPostRequest = { /** The size of disk storage for the session, in gigabytes */ disk_storage?: number; resource_class_id?: number | null; - cloudstorage?: SessionCloudStoragePostList; + data_connectors_overrides?: SessionDataConnectorsOverrideList; env_variable_overrides?: EnvVariableOverrides; }; export type SessionListResponse = SessionResponse[]; diff --git a/client/src/features/sessionsV2/api/sessionsV2.openapi.json b/client/src/features/sessionsV2/api/sessionsV2.openapi.json index bd470f72f9..6481d3ca7c 100644 --- a/client/src/features/sessionsV2/api/sessionsV2.openapi.json +++ b/client/src/features/sessionsV2/api/sessionsV2.openapi.json @@ -34,7 +34,8 @@ "$ref": "#/components/schemas/ImageCheckResponse" } } - } + }, + "description": "The image check has completed successfully" }, "422": { "content": { @@ -1241,17 +1242,15 @@ "$ref": "#/components/schemas/Ulid" }, "disk_storage": { - "default": 1, "type": "integer", "description": "The size of disk storage for the session, in gigabytes" }, "resource_class_id": { - "default": null, "nullable": true, "type": "integer" }, - "cloudstorage": { - "$ref": "#/components/schemas/SessionCloudStoragePostList" + "data_connectors_overrides": { + "$ref": "#/components/schemas/SessionDataConnectorsOverrideList" }, "env_variable_overrides": { "$ref": "#/components/schemas/EnvVariableOverrides" @@ -1405,40 +1404,80 @@ "maxLength": 26, "pattern": "^[0-7][0-9A-HJKMNP-TV-Z]{25}$" }, - "SessionCloudStoragePostList": { + "SessionDataConnectorsOverrideList": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionCloudStoragePost" + "$ref": "#/components/schemas/SessionDataConnectorOverride" } }, - "SessionCloudStoragePost": { + "SessionDataConnectorOverride": { "type": "object", "properties": { - "configuration": { - "type": "object", - "additionalProperties": true - }, - "readonly": { - "type": "boolean" - }, - "source_path": { - "type": "string" - }, - "target_path": { - "type": "string" + "skip": { + "type": "boolean", + "description": "The corresponding data connector will not be mounted if `skip` is set to `true`.", + "default": false }, - "storage_id": { + "data_connector_id": { "allOf": [ { "$ref": "#/components/schemas/Ulid" }, { - "description": "If the storage_id is provided then this config must replace an existing storage config in the session" + "description": "The `data_connector_id` must match an existing data connector from the session launcher's project.\n" } ] + }, + "configuration": { + "$ref": "#/components/schemas/RCloneConfig" + }, + "source_path": { + "$ref": "#/components/schemas/SourcePath" + }, + "target_path": { + "$ref": "#/components/schemas/TargetPath" + }, + "readonly": { + "$ref": "#/components/schemas/StorageReadOnly" } }, - "required": ["storage_id"] + "required": ["data_connector_id"] + }, + "RCloneConfig": { + "type": "object", + "description": "Dictionary of rclone key:value pairs (based on schema from '/storage_schema')", + "additionalProperties": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "nullable": true + }, + { + "type": "boolean" + }, + { + "type": "object" + } + ] + } + }, + "SourcePath": { + "description": "the source path to mount, usually starts with bucket/container name", + "type": "string", + "example": "bucket/my/storage/folder/" + }, + "TargetPath": { + "description": "the target path relative to the working directory where the storage should be mounted", + "type": "string", + "example": "my/project/folder" + }, + "StorageReadOnly": { + "description": "Whether this storage should be mounted readonly or not", + "type": "boolean", + "default": true }, "ServerName": { "type": "string", diff --git a/client/src/features/sessionsV2/startSessionOptionsV2.slice.ts b/client/src/features/sessionsV2/startSessionOptionsV2.slice.ts index ab4f81fe8e..c7c6b725b9 100644 --- a/client/src/features/sessionsV2/startSessionOptionsV2.slice.ts +++ b/client/src/features/sessionsV2/startSessionOptionsV2.slice.ts @@ -27,7 +27,7 @@ import { } from "./startSessionOptionsV2.types"; const initialState: StartSessionOptionsV2 = { - cloudStorage: undefined, + dataConnectors: undefined, defaultUrl: "", environmentVariables: [], imageReady: false, @@ -42,20 +42,23 @@ const startSessionOptionsV2Slice = createSlice({ name: "startSessionOptionsV2", initialState, reducers: { - addCloudStorageItem: ( + addDataConnectorOverrideItem: ( state, action: PayloadAction ) => { - state.cloudStorage?.push(action.payload); + if (state.dataConnectors == null) { + state.dataConnectors = []; + } + state.dataConnectors.push(action.payload); }, addEnvironmentVariable: (state) => { state.environmentVariables.push({ name: "", value: "" }); }, - removeCloudStorageItem: ( + removeDataConnectorOverrideItem: ( state, action: PayloadAction<{ index: number }> ) => { - state.cloudStorage?.splice(action.payload.index, 1); + state.dataConnectors?.splice(action.payload.index, 1); }, removeEnvironmentVariable: ( state, @@ -63,11 +66,11 @@ const startSessionOptionsV2Slice = createSlice({ ) => { state.environmentVariables.splice(action.payload.index, 1); }, - setCloudStorage: ( + setDataConnectorsOverrides: ( state, action: PayloadAction ) => { - state.cloudStorage = action.payload; + state.dataConnectors = action.payload; }, setDefaultUrl: (state, action: PayloadAction) => { state.defaultUrl = action.payload; @@ -90,15 +93,16 @@ const startSessionOptionsV2Slice = createSlice({ setUserSecretsReady: (state, action: PayloadAction) => { state.userSecretsReady = action.payload; }, - updateCloudStorageItem: ( + updateDataConnectorOverrideItem: ( state, action: PayloadAction<{ index: number; storage: SessionStartDataConnectorConfiguration; }> ) => { - if (state.cloudStorage) - state.cloudStorage[action.payload.index] = action.payload.storage; + if (state.dataConnectors) { + state.dataConnectors[action.payload.index] = action.payload.storage; + } }, updateEnvironmentVariable: ( state, diff --git a/client/src/features/sessionsV2/startSessionOptionsV2.types.ts b/client/src/features/sessionsV2/startSessionOptionsV2.types.ts index ab3b1e1f65..197bbd8a96 100644 --- a/client/src/features/sessionsV2/startSessionOptionsV2.types.ts +++ b/client/src/features/sessionsV2/startSessionOptionsV2.types.ts @@ -21,6 +21,8 @@ import type { DataConnectorRead } from "../dataConnectorsV2/api/data-connectors. export interface SessionStartDataConnectorConfiguration { active: boolean; + skip: boolean; + touched: boolean; dataConnector: DataConnectorRead; sensitiveFieldDefinitions: { friendlyName: string; @@ -33,7 +35,7 @@ export interface SessionStartDataConnectorConfiguration { } export interface StartSessionOptionsV2 { - cloudStorage?: SessionStartDataConnectorConfiguration[]; + dataConnectors?: SessionStartDataConnectorConfiguration[]; defaultUrl: string; environmentVariables: SessionEnvironmentVariable[]; imageReady: boolean; diff --git a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts index 6925e331e2..0545aaddd7 100644 --- a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts +++ b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts @@ -154,7 +154,7 @@ export default function useSessionLauncherState({ isReadyDataConnectorConfigs ) { dispatch( - startSessionOptionsV2Slice.actions.setCloudStorage( + startSessionOptionsV2Slice.actions.setDataConnectorsOverrides( initialDataConnectorConfigs ) ); diff --git a/tests/cypress/e2e/projectV2Session.spec.ts b/tests/cypress/e2e/projectV2Session.spec.ts index f3a7fa9ff3..1e7d58d042 100644 --- a/tests/cypress/e2e/projectV2Session.spec.ts +++ b/tests/cypress/e2e/projectV2Session.spec.ts @@ -99,8 +99,8 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(0); req.reply({ body: session, delay: 2000 }); }).as("createSession"); }); @@ -156,13 +156,15 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); - const storage = csConfig[0]; - expect(storage.configuration).to.have.property("access_key_id"); - expect(storage.configuration).to.have.property("secret_access_key"); - expect(storage.configuration["access_key_id"]).to.equal("access key"); - expect(storage.configuration["secret_access_key"]).to.equal( + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(1); + const override = dcOverrides[0]; + expect(override.skip).to.be.false; + expect(override.data_connector_id).to.equal("ULID-1"); + expect(override.configuration).to.have.property("access_key_id"); + expect(override.configuration).to.have.property("secret_access_key"); + expect(override.configuration["access_key_id"]).to.equal("access key"); + expect(override.configuration["secret_access_key"]).to.equal( "secret key" ); req.reply({ body: session, delay: 2000 }); @@ -237,13 +239,8 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); - const storage = csConfig[0]; - // Since the session has already been saved, it doesn't need to be sent again - expect(storage.configuration).to.not.have.property("access_key_id"); - expect(storage.configuration).to.not.have.property("secret_access_key"); - + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(0); req.reply({ body: session, delay: 2000 }); }).as("createSession"); }); @@ -297,7 +294,7 @@ describe("launch sessions with data connectors", () => { cy.url().should("match", /\/p\/.*\/sessions\/show\/.*/); }); - it("launch session with data connector, saving credentials on skip", () => { + it("launch session with skipped data connector", () => { fixtures .testCloudStorage() .listProjectDataConnectors() @@ -330,11 +327,11 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); - const storage = csConfig[0]; - expect(storage.configuration).to.not.have.property("access_key_id"); - expect(storage.configuration).to.not.have.property("secret_access_key"); + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(1); + const override = dcOverrides[0]; + expect(override.skip).to.be.true; + expect(override.data_connector_id).to.equal("ULID-1"); req.reply({ body: session, delay: 2000 }); }).as("createSession"); }); @@ -368,9 +365,6 @@ describe("launch sessions with data connectors", () => { cy.getDataCy("session-data-connector-credentials-modal") .contains("Skip") .click(); - cy.contains("Saving credentials...").should("be.visible"); - cy.wait("@patchDataConnectorSecrets"); - cy.wait("@getDataConnectorSecretsAfterSaving"); cy.wait("@createSession"); cy.url().should("match", /\/p\/.*\/sessions\/show\/.*/); }); @@ -393,12 +387,8 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); - const storage = csConfig[0]; - expect(storage.storage_id).to.equal("ULID-1"); - expect(storage.configuration).to.not.have.property("access_key_id"); - expect(storage.configuration).to.not.have.property("secret_access_key"); + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(0); req.reply({ body: session, delay: 2000 }); }).as("createSession"); }); @@ -432,14 +422,15 @@ describe("launch sessions with data connectors", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); - const storage = csConfig[0]; - expect(storage.storage_id).to.equal("ULID-1"); - expect(storage.configuration).to.have.property("access_key_id"); - expect(storage.configuration).to.have.property("secret_access_key"); - expect(storage.configuration["access_key_id"]).to.equal("access key"); - expect(storage.configuration["secret_access_key"]).to.equal( + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(1); + const override = dcOverrides[0]; + expect(override.skip).to.be.false; + expect(override.data_connector_id).to.equal("ULID-1"); + expect(override.configuration).to.have.property("access_key_id"); + expect(override.configuration).to.have.property("secret_access_key"); + expect(override.configuration["access_key_id"]).to.equal("access key"); + expect(override.configuration["secret_access_key"]).to.equal( "secret key" ); req.reply({ body: session, delay: 2000 }); @@ -1022,8 +1013,8 @@ describe("view autostart link", () => { cy.fixture("sessions/sessionV2.json").then((session) => { // eslint-disable-next-line max-nested-callbacks cy.intercept("POST", "/api/data/sessions", (req) => { - const csConfig = req.body.cloudstorage; - expect(csConfig.length).equal(1); + const dcOverrides = req.body.data_connectors_overrides; + expect(dcOverrides).to.have.length(0); req.reply({ body: session, delay: 2000 }); }).as("createSession"); });