Skip to content

Commit 56b4c75

Browse files
pierrebeitzjanisz
authored andcommitted
feat: add depency selection to jobs form
when adding a job, you can now select dependency-jobs in the general tab when your cluster version is >= 2.2.0. closes D2IQ-71316
1 parent ad2cda5 commit 56b4c75

File tree

10 files changed

+119
-12
lines changed

10 files changed

+119
-12
lines changed

locale/en/messages.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@
758758
"No health checks available": "",
759759
"No linked clusters": "",
760760
"No log files found on this agent.": "",
761+
"No more jobs to choose from.": "",
761762
"No nodes detected": "",
762763
"No package repositories": "",
763764
"No quota defined": "",
@@ -1131,6 +1132,7 @@
11311132
"This host declined an offer because it has scarce resources (e.g. GPUs).": "",
11321133
"This host port will be accessible as an environment variable called '$PORT{index}'. <0>More information</0>.": "",
11331134
"This host port will be accessible as an environment variable called {environmentVariableName}. <0>More information</0>.": "",
1135+
"This job will run whenever its last successful run is older than all of its dependencies' latest successful runs.": "",
11341136
"This package can only be installed using the CLI. See the <0>documentation</0>.": "",
11351137
"This package is known to have issues when started with its default settings.": "",
11361138
"This port will be used to load balance this service address internally": "",

locale/zh/messages.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@
758758
"No health checks available": "无可用的运行状况检查",
759759
"No linked clusters": "无链接集群",
760760
"No log files found on this agent.": "此代理上未找到日志文件。",
761+
"No more jobs to choose from.": "",
761762
"No nodes detected": "未检测到节点",
762763
"No package repositories": "无包存储库",
763764
"No quota defined": "未定义配额",
@@ -1131,6 +1132,7 @@
11311132
"This host declined an offer because it has scarce resources (e.g. GPUs).": "",
11321133
"This host port will be accessible as an environment variable called '$PORT{index}'. <0>More information</0>.": "此主机端口可以作为一个名为 '$PORT{index}' 的环境变量进行访问。<0>更多信息</0>.",
11331134
"This host port will be accessible as an environment variable called {environmentVariableName}. <0>More information</0>.": "此主机端口可以作为一个名为 {environmentVariableName} 的环境变量进行访问。<0>更多信息</0>.",
1135+
"This job will run whenever its last successful run is older than all of its dependencies' latest successful runs.": "",
11341136
"This package can only be installed using the CLI. See the <0>documentation</0>.": "此包仅可使用 CLI 进行安装。查看 <0>文档</0>.",
11351137
"This package is known to have issues when started with its default settings.": "",
11361138
"This port will be used to load balance this service address internally": "此端口将用于在内部负载均衡此服务地址",

plugins/jobs/src/js/components/form/GeneralFormSection.tsx

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Trans } from "@lingui/macro";
22
import * as React from "react";
33
import {
4+
SpacingBox,
5+
Typeahead,
46
Textarea,
7+
TextInputWithBadges,
58
TextInput,
69
Tooltip,
710
} from "@dcos/ui-kit";
@@ -20,17 +23,31 @@ import {
2023
FormOutput,
2124
FormError,
2225
Action,
23-
JobFormActionType,
26+
JobFormActionType as A,
2427
} from "./helpers/JobFormData";
28+
import { fetchJobs } from "#SRC/js/events/MetronomeClient";
29+
import dcosVersion$ from "#SRC/js/stores/dcos-version";
2530

2631
const getFieldError = (path: string, errors: FormError[]) =>
2732
errors
2833
.filter((e) => e.path.join(".") === path)
2934
.map((e) => e.message)
3035
.join(" ");
3136

37+
export default class GeneralFormSection extends React.Component<{
38+
formData: FormOutput;
39+
errors: FormError[];
40+
showErrors: boolean;
41+
isEdit: boolean;
42+
onChange: (a: Action) => void;
43+
}> {
44+
state = { jobs: [], hasJobsWithDeps: false };
3245

33-
export default class GeneralFormSection extends React.Component<GeneralProps> {
46+
componentWillMount() {
47+
dcosVersion$.subscribe(({ hasJobsWithDeps }) => {
48+
this.setState({ hasJobsWithDeps });
49+
});
50+
}
3451
public getResourceRow() {
3552
const { formData, showErrors, errors } = this.props;
3653
const gpusDisabled = !formData.cmdOnly && formData.container !== "ucr";
@@ -72,7 +89,7 @@ export default class GeneralFormSection extends React.Component<GeneralProps> {
7289
<FormGroup className="column-3" showError={showErrors && !!diskError}>
7390
<TextInput
7491
required={true}
75-
inputLabel={<Trans id="disk (MiB)" />}
92+
inputLabel={<Trans id="Disk (MiB)" />}
7693
name="job.run.disk"
7794
type="number"
7895
value={formData.disk}
@@ -239,8 +256,84 @@ export default class GeneralFormSection extends React.Component<GeneralProps> {
239256
</FormGroup>
240257
</FormRow>
241258
{this.getResourceRow()}
259+
260+
{this.state.hasJobsWithDeps && (
261+
<DependencyPicker
262+
deps={formData.dependencies}
263+
onChange={this.props.onChange}
264+
error={getFieldError("dependencies", errors)}
265+
/>
266+
)}
267+
242268
{this.getJobType()}
243269
</div>
244270
);
245271
}
246272
}
273+
274+
const setDeps = (value) => ({ path: "job.dependencies", type: A.Set, value });
275+
type Option = { label: string; value: string };
276+
const DependencyPicker: React.FC<{
277+
deps: Array<{ id: string }>;
278+
onChange: (a: Action) => void;
279+
error: string;
280+
}> = ({ deps = [], onChange, error }) => {
281+
const [q, setQ] = React.useState("");
282+
const [jobs, setJobs] = React.useState<Option[]>([]);
283+
284+
React.useEffect(() => {
285+
fetchJobs().subscribe(({ response }) =>
286+
setJobs(response.map((job) => ({ label: job.id, value: job.id })))
287+
);
288+
}, []);
289+
290+
const filteredJobs = jobs.filter(
291+
({ label }) => label.includes(q) && !deps.map((d) => d.id).includes(label)
292+
);
293+
const addJob = ([id]: string[]) => {
294+
setQ("");
295+
onChange(setDeps([...deps, { id }]));
296+
};
297+
const badgeChangeHandler = (leftoverBadges: Array<{ value: string }>) => {
298+
onChange(setDeps([...leftoverBadges.map((b) => ({ id: b.value }))]));
299+
};
300+
const onChangeQ = (e) => {
301+
setQ(e.currentTarget.value);
302+
e.stopPropagation();
303+
};
304+
305+
const toBadge = ({ id }) => ({ value: id, label: id });
306+
307+
return (
308+
<FormRow>
309+
<FormGroup className="column-12" showError={!!error}>
310+
<Typeahead
311+
items={filteredJobs}
312+
overlayRoot={document.querySelector<HTMLElement>(".modal-wrapper")!}
313+
resetInputOnSelect={true}
314+
menuEmptyState={
315+
<SpacingBox>
316+
<Trans id="No more jobs to choose from." />
317+
</SpacingBox>
318+
}
319+
textField={
320+
<TextInputWithBadges
321+
badges={deps.map(toBadge)}
322+
inputLabel={<Trans id="Dependencies" />}
323+
onBadgeChange={badgeChangeHandler}
324+
onChange={onChangeQ}
325+
placeholder={deps.length ? "" : "Select a Job"}
326+
tooltipContent={
327+
<Trans id="This job will run whenever its last successful run is older than all of its dependencies' latest successful runs." />
328+
}
329+
value={q}
330+
/>
331+
}
332+
onSelect={addJob}
333+
/>
334+
335+
<FieldError>{error}</FieldError>
336+
</FormGroup>
337+
</FormRow>
338+
);
339+
};

plugins/jobs/src/js/components/form/helpers/JobFormData.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface Job<Labels, Env, Secrets> {
33
description?: string;
44
labels?: Labels;
55
run: JobRun<Env, Secrets>;
6+
dependencies?: Array<{ id: string }>;
67
}
78

89
export interface JobRun<Env, Secrets> {
@@ -108,6 +109,7 @@ export interface FormOutput {
108109
artifacts?: JobArtifact[];
109110
volumes: Array<SecretVolume | JobVolume>;
110111
placementConstraints?: PlacementConstraint[];
112+
dependencies?: Array<{ id: string }>;
111113
}
112114

113115
// Labels used internally to track form state

plugins/jobs/src/js/components/form/helpers/JobParsers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export const jobSpecToFormOutputParser = (jobSpec: JobSpec): FormOutput => {
201201
}
202202

203203
return {
204+
dependencies: jobSpec.job.dependencies,
204205
jobId: jobSpec.job.id,
205206
description: jobSpec.job.description,
206207
cmdOnly: jobSpec.cmdOnly,

src/js/__tests__/__snapshots__/typecheck-test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ plugins/jobs/src/js/__tests__/jobsToggleSchedule-test.ts: error TS2339: Property
369369
plugins/jobs/src/js/__tests__/jobsToggleSchedule-test.ts: error TS2339: Property 'mockReturnValue' does not exist on type *.
370370
plugins/jobs/src/js/components/JobsForm.tsx: error TS2322: type * is not assignable to type *.
371371
plugins/jobs/src/js/components/JobsForm.tsx: error TS2322: type * is not assignable to type *.
372+
plugins/jobs/src/js/components/JobsForm.tsx: error TS2322: type * is not assignable to type *.
372373
plugins/jobs/src/js/components/JobsFormConfig.tsx: error TS2322: type * is not assignable to type *.
373374
plugins/jobs/src/js/components/JobsOverviewList.tsx: error TS2769: No overload matches this call.
374375
plugins/jobs/src/js/components/form/ArgsSection.tsx: error TS2322: Type 'void' is not assignable to type *.

src/js/events/__tests__/MetronomeClient-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe("MetronomeClient", () => {
4848
},
4949
artifacts: [],
5050
secrets: {},
51+
dependencies: [{ id: "dep" }],
5152
},
5253
};
5354
const jobData: JobResponse = {

src/js/stores/dcos-version.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default fromFetch(
2626
variant: json["dcos-variant"] as string,
2727
// we could just pass through the version itself. but having long identifiers will make it easier to understand why some code is conditional and also to remove that conditional again.
2828
hasQuotaSupport: Version.compare(json.version, "2.0.0-alpha") >= 0,
29+
hasJobsWithDeps: Version.compare(json.version, "2.2.0-alpha") >= 0,
2930
hasCalicoNetworking: Version.compare(json.version, "2.1.0-alpha") >= 0,
3031
hasVerticalBursting: Version.compare(json.version, "2.1.0-alpha") >= 0,
3132
}))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2.1.0",
2+
"version": "2.2.0",
33
"dcos-image-commit": "43eff42b8eb9c5bf5d27b0d56e42b9ffe9ae2b32",
44
"bootstrap-id": "88d5b8c52eba580b9ee8418410a0d85cfe4c19e5"
55
}

tests/pages/jobs/JobCreateForm-cy.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ describe("Job Create Form", () => {
3636
});
3737

3838
context("General tab", () => {
39-
it("displays an error badge for the missing id", () => {
39+
it("displays an error when id is missing", () => {
4040
// Fill-in the input elements
41-
cy.root().get("label").contains("Command Only").click();
42-
cy.root()
43-
.getFormGroupInputFor("Command *")
44-
.type("while true; do echo 'test' ; sleep 100 ; done");
41+
cy.get("label").contains("Command Only").click();
42+
cy.getFormGroupInputFor("Command *").type(
43+
"while true; do echo 'test' ; sleep 100 ; done"
44+
);
4545

4646
// Try to submit the form
4747
submit();
@@ -55,13 +55,17 @@ describe("Job Create Form", () => {
5555
getActiveTabErrorBadge().contains("1");
5656

5757
// Fix error
58-
cy.root()
59-
.getFormGroupInputFor("Job ID *")
60-
.type(`{selectall}${"simple"}`);
58+
cy.getFormGroupInputFor("Job ID *").retype("simple");
6159

6260
// Error badge disappears
6361
getActiveTabErrorBadge().should("not.be.visible");
6462
});
63+
64+
it("adds and removes a dependency", () => {
65+
cy.get("label:contains(Dependencies)").click();
66+
cy.get("[data-cy=PopoverListItem]:contains(bar)").click();
67+
cy.get("[data-cy=badge]:contains(bar) [role=button]").click();
68+
});
6569
});
6670

6771
context("Container Runtime tab", () => {

0 commit comments

Comments
 (0)