Skip to content

Commit 81ee45c

Browse files
committed
feat: add job submit flow
1 parent 6f02989 commit 81ee45c

40 files changed

Lines changed: 2472 additions & 479 deletions

client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface UseProjectPermissionsArgs {
3030
export default function useProjectPermissions({
3131
projectId,
3232
}: UseProjectPermissionsArgs): PermissionsWithLoadingState {
33-
const { currentData, isLoading, isError, isUninitialized } =
33+
const { currentData, isLoading, isError, isUninitialized, error } =
3434
projectV2Api.endpoints.getProjectsByProjectIdPermissions.useQueryState(
3535
projectId ? { projectId } : skipToken,
3636
);
@@ -44,17 +44,25 @@ export default function useProjectPermissions({
4444
}, [fetchPermissions, isUninitialized, projectId]);
4545

4646
const isLoadingPermissions = isLoading || !!(projectId && isUninitialized);
47+
const arePermissionsResolved =
48+
!isLoadingPermissions && !isError && currentData != null;
4749

4850
if (isLoading || isError || !currentData) {
4951
return {
5052
...DEFAULT_PERMISSIONS,
53+
arePermissionsResolved: false,
5154
isLoadingPermissions,
55+
isPermissionsError: isError,
56+
permissionsError: isError ? error : undefined,
5257
};
5358
}
5459

5560
return {
5661
...DEFAULT_PERMISSIONS,
5762
...currentData,
63+
arePermissionsResolved,
5864
isLoadingPermissions: false,
65+
isPermissionsError: false,
66+
permissionsError: undefined,
5967
};
6068
}

client/src/features/permissionsV2/permissions.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,10 @@ export type Permissions = {
2323
};
2424

2525
export type PermissionsWithLoadingState = Permissions & {
26+
arePermissionsResolved: boolean;
2627
isLoadingPermissions: boolean;
28+
isPermissionsError: boolean;
29+
permissionsError?: unknown;
2730
};
31+
32+
export type ProjectPermissions = PermissionsWithLoadingState;

client/src/features/sessionsV2/DataConnectorSecretsModal.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ const CONTEXT_STRINGS = {
5959
testError:
6060
"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.",
6161
},
62+
job: {
63+
continueButton: "Continue",
64+
dataCy: "job-data-connector-credentials-modal",
65+
header: "Job Storage Credentials",
66+
testError:
67+
"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 job.",
68+
},
6269
storage: {
6370
continueButton: "Test and Save",
6471
dataCy: "data-connector-credentials-modal",
@@ -158,7 +165,7 @@ function DataConnectorSecrets({
158165
}
159166

160167
interface DataConnectorSecretsModalProps {
161-
context?: "session" | "storage";
168+
context?: "session" | "job" | "storage";
162169
isOpen: boolean;
163170
onCancel: () => void;
164171
onStart: (dataConnectorConfigs: DataConnectorConfiguration[]) => void;
@@ -352,7 +359,9 @@ function CredentialsButtons({
352359
<XLg className={cx("bi", "me-1")} />
353360
Cancel
354361
</Button>
355-
{context === "session" && <SkipConnectionTestButton onSkip={onSkip} />}
362+
{(context === "session" || context === "job") && (
363+
<SkipConnectionTestButton context={context} onSkip={onSkip} />
364+
)}
356365
{context === "storage" && (
357366
<ClearCredentialsButton
358367
onSkip={onSkip}
@@ -612,9 +621,11 @@ function SensitiveFieldInput({
612621
}
613622

614623
function SkipConnectionTestButton({
624+
context,
615625
onSkip,
616-
}: Pick<CredentialsButtonsProps, "onSkip">) {
626+
}: Pick<CredentialsButtonsProps, "context" | "onSkip">) {
617627
const skipButtonRef = useRef<HTMLAnchorElement>(null);
628+
const targetLabel = context === "job" ? "job" : "session";
618629
return (
619630
<>
620631
<span ref={skipButtonRef}>
@@ -624,7 +635,7 @@ function SkipConnectionTestButton({
624635
</Button>
625636
</span>
626637
<UncontrolledTooltip target={skipButtonRef}>
627-
Skip the data connector. It will not be mounted in the session.
638+
{`Skip the data connector. It will not be mounted in the ${targetLabel}.`}
628639
</UncontrolledTooltip>
629640
</>
630641
);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*!
2+
* Copyright 2026 - Swiss Data Science Center (SDSC)
3+
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4+
* Eidgenössische Technische Hochschule Zürich (ETHZ).
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import cx from "classnames";
20+
import { useEffect, useMemo, useRef, useState } from "react";
21+
22+
import RtkOrDataServicesError from "~/components/errors/RtkOrDataServicesError";
23+
import ProgressStepsIndicator, {
24+
ProgressStyle,
25+
ProgressType,
26+
StatusStepProgressBar,
27+
StepsProgressBar,
28+
} from "~/components/progress/ProgressSteps";
29+
import { storageDefinitionAfterSavingCredentialsFromConfig } from "../cloudStorage/projectCloudStorage.utils";
30+
import { usePatchDataConnectorsByDataConnectorIdSecretsMutation } from "../dataConnectorsV2/api/data-connectors.enhanced-api";
31+
import { shouldCloudStorageSaveCredentials } from "./sessionLaunchValidation.utils";
32+
import type { SessionStartDataConnectorConfiguration } from "./startSessionOptionsV2.types";
33+
34+
import progressBoxStyles from "~/components/progress/ProgressBox.module.scss";
35+
36+
interface SaveCloudStorageCredentialsProps {
37+
dataConnectors: SessionStartDataConnectorConfiguration[];
38+
onComplete: (configs: SessionStartDataConnectorConfiguration[]) => void;
39+
title?: string;
40+
}
41+
42+
export default function SaveCloudStorageCredentials({
43+
dataConnectors,
44+
onComplete,
45+
title = "Saving credentials",
46+
}: SaveCloudStorageCredentialsProps) {
47+
const [steps, setSteps] = useState<StepsProgressBar[]>([]);
48+
const [saveCredentials, saveCredentialsResult] =
49+
usePatchDataConnectorsByDataConnectorIdSecretsMutation();
50+
51+
const credentialsToSave = useMemo(() => {
52+
return dataConnectors
53+
.filter(shouldCloudStorageSaveCredentials)
54+
.map((cs) => ({
55+
storageName: cs.dataConnector.name,
56+
storageId: cs.dataConnector.id,
57+
secrets: cs.sensitiveFieldValues,
58+
}));
59+
}, [dataConnectors]);
60+
61+
const [results, setResults] = useState<StatusStepProgressBar[]>(
62+
credentialsToSave.map(() => StatusStepProgressBar.WAITING),
63+
);
64+
65+
const [index, setIndex] = useState(0);
66+
const [hasFailed, setHasFailed] = useState(false);
67+
const [failedError, setFailedError] =
68+
useState<typeof saveCredentialsResult.error>(undefined);
69+
const hasCompletedRef = useRef(false);
70+
71+
useEffect(() => {
72+
const theSteps = credentialsToSave.map((cs, i) => ({
73+
id: i,
74+
status: results[i],
75+
step: `Saving credentials for ${cs.storageName}`,
76+
}));
77+
// TODO: fix react-hooks/set-state-in-effect
78+
// eslint-disable-next-line react-hooks/set-state-in-effect
79+
setSteps(theSteps);
80+
}, [credentialsToSave, results]);
81+
82+
useEffect(() => {
83+
if (
84+
hasFailed ||
85+
credentialsToSave.length < 1 ||
86+
index >= credentialsToSave.length
87+
)
88+
return;
89+
// TODO: fix react-hooks/set-state-in-effect
90+
// eslint-disable-next-line react-hooks/set-state-in-effect
91+
setResults((prev) => {
92+
const newResults = [...prev];
93+
newResults[index] = StatusStepProgressBar.EXECUTING;
94+
return newResults;
95+
});
96+
const storage = credentialsToSave[index];
97+
saveCredentials({
98+
dataConnectorId: storage.storageId,
99+
dataConnectorSecretPatchList: Object.entries(storage.secrets).map(
100+
([key, value]) => ({
101+
name: key,
102+
value,
103+
}),
104+
),
105+
});
106+
}, [credentialsToSave, hasFailed, index, saveCredentials]);
107+
108+
useEffect(() => {
109+
if (
110+
saveCredentialsResult.isUninitialized ||
111+
saveCredentialsResult.isLoading
112+
)
113+
return;
114+
if (saveCredentialsResult.data != null) {
115+
// TODO: fix react-hooks/set-state-in-effect
116+
// eslint-disable-next-line react-hooks/set-state-in-effect
117+
setResults((prev) => {
118+
const newResults = [...prev];
119+
newResults[index] = StatusStepProgressBar.READY;
120+
return newResults;
121+
});
122+
saveCredentialsResult.reset();
123+
setIndex((prev) => prev + 1);
124+
return;
125+
}
126+
if (saveCredentialsResult.error != null) {
127+
setResults((prev) => {
128+
const newResults = [...prev];
129+
newResults[index] = StatusStepProgressBar.FAILED;
130+
return newResults;
131+
});
132+
setFailedError(saveCredentialsResult.error);
133+
setHasFailed(true);
134+
saveCredentialsResult.reset();
135+
}
136+
}, [index, saveCredentialsResult]);
137+
138+
useEffect(() => {
139+
if (
140+
hasFailed ||
141+
saveCredentialsResult.isLoading ||
142+
hasCompletedRef.current
143+
) {
144+
return;
145+
}
146+
if (index >= credentialsToSave.length) {
147+
hasCompletedRef.current = true;
148+
const cloudStorageConfigs = dataConnectors.map((cs) =>
149+
storageDefinitionAfterSavingCredentialsFromConfig(cs),
150+
);
151+
onComplete(cloudStorageConfigs);
152+
}
153+
}, [
154+
credentialsToSave.length,
155+
dataConnectors,
156+
hasFailed,
157+
index,
158+
onComplete,
159+
saveCredentialsResult.isLoading,
160+
]);
161+
162+
return (
163+
<div
164+
className={cx(
165+
progressBoxStyles.progressBoxSmall,
166+
progressBoxStyles.progressBoxSmallSteps,
167+
)}
168+
>
169+
{hasFailed && failedError != null && (
170+
<RtkOrDataServicesError
171+
dismissible={false}
172+
error={failedError as never}
173+
/>
174+
)}
175+
<ProgressStepsIndicator
176+
description="Saving credentials..."
177+
type={ProgressType.Determinate}
178+
style={ProgressStyle.Light}
179+
title={title}
180+
status={steps}
181+
/>
182+
</div>
183+
);
184+
}

client/src/features/sessionsV2/SessionList/SessionCard.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import cx from "classnames";
2020
import { Col, Row } from "reactstrap";
2121

22+
import {
23+
getLauncherCategoryDefinition,
24+
sessionLauncherKindToCategory,
25+
} from "~/features/sessionsV2/session.utils";
2226
import { Project } from "../../projectsV2/api/projectV2.api";
2327
import ActiveSessionButton from "../components/SessionButton/ActiveSessionButton";
2428
import {
@@ -38,7 +42,9 @@ interface SessionCardProps {
3842
export default function SessionCard({ project, session }: SessionCardProps) {
3943
if (!session) return null;
4044

45+
const launcherCategory = sessionLauncherKindToCategory(session.session_type);
4146
const stylesPerSession = getSessionStatusStyles(session);
47+
const launcherDefinition = getLauncherCategoryDefinition(launcherCategory);
4248

4349
return (
4450
<div
@@ -50,13 +56,21 @@ export default function SessionCard({ project, session }: SessionCardProps) {
5056
"pb-3",
5157
)}
5258
>
53-
<img
54-
src={stylesPerSession.sessionLine}
55-
className={cx("position-absolute", styles.SessionLine)}
56-
alt="Session line indicator"
57-
loading="lazy"
58-
/>
59-
<div className={cx("ms-5", "px-3", "pt-3")}>
59+
{launcherCategory === "session" && (
60+
<img
61+
src={stylesPerSession.sessionLine}
62+
className={cx("position-absolute", styles.SessionLine)}
63+
alt="Session line indicator"
64+
loading="lazy"
65+
/>
66+
)}
67+
<div
68+
className={cx(
69+
launcherCategory === "session" ? "ms-5" : "ms-2",
70+
"px-3",
71+
"pt-3",
72+
)}
73+
>
6074
<Row className="g-2">
6175
<Col xs={12} xl="auto">
6276
<Row className="g-2">
@@ -71,7 +85,8 @@ export default function SessionCard({ project, session }: SessionCardProps) {
7185
)}
7286
>
7387
<span className={cx("small", "text-muted", "me-3")}>
74-
Session
88+
{launcherDefinition.text.display}
89+
{session.submission_id ? `: ${session.submission_id}` : ""}
7590
</span>
7691
</Col>
7792
<Col

0 commit comments

Comments
 (0)