Skip to content

Commit 127f524

Browse files
committed
course: configuration copy -- start implementing actual work
1 parent 455dafa commit 127f524

File tree

2 files changed

+137
-33
lines changed

2 files changed

+137
-33
lines changed

src/packages/frontend/course/configuration/actions.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ import { DEFAULT_LICENSE_UPGRADE_HOST_PROJECT } from "../store";
2121
import { SiteLicenseStrategy, SyncDBRecord, UpgradeGoal } from "../types";
2222
import { StudentProjectFunctionality } from "./customize-student-project-functionality";
2323
import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
24+
import { delay } from "awaiting";
25+
26+
export const CONFIGURATION_GROUPS = [
27+
"collaborator-policy",
28+
"email-invitation",
29+
"copy-limit",
30+
"restrict-student-projects",
31+
"nbgrader",
32+
"network-file-systems",
33+
"env-variables",
34+
"upgrades",
35+
"software-environment",
36+
] as const;
37+
38+
export type ConfigurationGroup = (typeof CONFIGURATION_GROUPS)[number];
39+
40+
interface ConfigurationTarget {
41+
project_id: string;
42+
path: string;
43+
}
2444

2545
export class ConfigurationActions {
2646
private course_actions: CourseActions;
@@ -409,4 +429,58 @@ export class ConfigurationActions {
409429
}
410430
syncdb.commit();
411431
};
432+
433+
copyConfiguration = async ({
434+
groups,
435+
targets,
436+
}: {
437+
groups: ConfigurationGroup[];
438+
targets: ConfigurationTarget[];
439+
}) => {
440+
console.log("copyConfiguration", { groups, targets });
441+
const store = this.course_actions.get_store();
442+
if (groups.length == 0 || targets.length == 0 || store == null) {
443+
return;
444+
}
445+
const settings = store.get("settings");
446+
for (const target of targets) {
447+
const targetActions = await openCourseFileAndGetActions({
448+
...target,
449+
maxTimeMs: 30000,
450+
});
451+
for (const group of groups) {
452+
console.log("copyConfiguration: copy ", target, {
453+
allow_collabs: !!settings.get("allow_collabs"),
454+
table: "settings",
455+
});
456+
if (group == "collaborator-policy") {
457+
const allow_colabs = !!settings.get("allow_collabs");
458+
targetActions.course_actions.configuration.set_allow_collabs(
459+
allow_colabs,
460+
);
461+
}
462+
targetActions.course_actions.syncdb.save();
463+
}
464+
}
465+
// switch back
466+
const { project_id, path } = this.course_actions.syncdb;
467+
redux.getProjectActions(project_id).open_file({ path, foreground: true });
468+
};
469+
}
470+
471+
async function openCourseFileAndGetActions({ project_id, path, maxTimeMs }) {
472+
await redux
473+
.getProjectActions(project_id)
474+
.open_file({ path, foreground: true });
475+
const t = Date.now();
476+
let d = 250;
477+
while (Date.now() + d - t <= maxTimeMs) {
478+
await delay(d);
479+
const targetActions = redux.getEditorActions(project_id, path);
480+
if (targetActions?.course_actions?.syncdb.get_state() == "ready") {
481+
return targetActions;
482+
}
483+
d *= 1.1;
484+
}
485+
throw Error(`unable to open '${path}'`);
412486
}

src/packages/frontend/course/configuration/configuration-copying.tsx

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,11 @@ import { COMMANDS } from "@cocalc/frontend/course/commands";
3737
import { ProjectTitle } from "@cocalc/frontend/projects/project-title";
3838
import { plural } from "@cocalc/util/misc";
3939
import { exec } from "@cocalc/frontend/frame-editors/generic/client";
40-
41-
const COPY_OPTIONS = [
42-
"collaborator-policy",
43-
"email-invitation",
44-
"copy-limit",
45-
"restrict-student-projects",
46-
"nbgrader",
47-
"network-file-systems",
48-
"env-variables",
49-
"upgrades",
50-
"software-environment",
51-
] as const;
40+
import { CONFIGURATION_GROUPS, ConfigurationGroup } from "./actions";
5241
import { useFrameContext } from "@cocalc/frontend/app-framework";
5342

54-
type CopyOptionKeys = (typeof COPY_OPTIONS)[number];
55-
5643
export type CopyConfigurationOptions = {
57-
[K in CopyOptionKeys]?: boolean;
44+
[K in ConfigurationGroup]?: boolean;
5845
};
5946

6047
export interface CopyConfigurationTargets {
@@ -74,13 +61,42 @@ export default function ConfigurationCopying({
7461
actions,
7562
close,
7663
}: Props) {
64+
const [error, setError] = useState<string>("");
7765
const { numTargets, numOptions } = useMemo(() => {
78-
const targets = (settings.get("copy_config_targets")?.toJS() ??
79-
{}) as CopyConfigurationTargets;
80-
const options = (settings.get("copy_config_options")?.toJS() ??
81-
{}) as CopyConfigurationOptions;
66+
const targets = getTargets(settings);
67+
const options = getOptions(settings);
8268
return { numTargets: numTrue(targets), numOptions: numTrue(options) };
8369
}, [settings]);
70+
const [copying, setCopying] = useState<boolean>(false);
71+
72+
const copyConfiguration = async () => {
73+
try {
74+
setCopying(true);
75+
setError("");
76+
const targets = getTargets(settings);
77+
const options = getOptions(settings);
78+
const t: { project_id: string; path: string }[] = [];
79+
for (const key in targets) {
80+
if (targets[key] === true) {
81+
t.push(parseKey(key));
82+
}
83+
}
84+
const g: ConfigurationGroup[] = [];
85+
for (const key in options) {
86+
if (options[key] === true) {
87+
g.push(key as ConfigurationGroup);
88+
}
89+
}
90+
await actions.configuration.copyConfiguration({
91+
groups: g,
92+
targets: t,
93+
});
94+
} catch (err) {
95+
setError(`${err}`);
96+
} finally {
97+
setCopying(false);
98+
}
99+
};
84100

85101
return (
86102
<Card
@@ -94,12 +110,18 @@ export default function ConfigurationCopying({
94110
Copy configuration from this course to other courses.
95111
</div>
96112
<div style={{ textAlign: "center", margin: "15px 0" }}>
97-
<Button size="large" disabled={numTargets == 0 || numOptions == 0}>
113+
<Button
114+
size="large"
115+
disabled={numTargets == 0 || numOptions == 0 || copying}
116+
onClick={copyConfiguration}
117+
>
98118
<Icon name="copy" />
99-
Copy {numOptions} {plural(numOptions, "configuration item")} to{" "}
100-
{numTargets} {plural(numTargets, "target course")}
119+
Copy{copying ? "ing" : ""} {numOptions}{" "}
120+
{plural(numOptions, "configuration item")} to {numTargets}{" "}
121+
{plural(numTargets, "target course")} {copying && <Spin />}
101122
</Button>
102123
</div>
124+
<ShowError style={{ margin: "15px" }} error={error} setError={setError} />
103125
<ConfigTargets
104126
actions={actions}
105127
project_id={project_id}
@@ -170,7 +192,8 @@ function ConfigTargets({
170192
</>
171193
) : undefined}
172194
</Checkbox>
173-
<Tooltip mouseEnterDelay={1}
195+
<Tooltip
196+
mouseEnterDelay={1}
174197
title={
175198
<>Open {path} in a new tab. (Use shift to open in background.)</>
176199
}
@@ -184,7 +207,9 @@ function ConfigTargets({
184207
redux
185208
.getProjectActions(project_id)
186209
.open_file({ path, foreground });
187-
close?.();
210+
if (foreground) {
211+
close?.();
212+
}
188213
}}
189214
>
190215
<Icon name="external-link" />
@@ -202,7 +227,10 @@ function ConfigTargets({
202227
actions.set({ copy_config_targets, table: "settings" });
203228
}}
204229
>
205-
<Tooltip mouseEnterDelay={1} title={<>Remove {path} from copy targets?</>}>
230+
<Tooltip
231+
mouseEnterDelay={1}
232+
title={<>Remove {path} from copy targets?</>}
233+
>
206234
<Button size="small" type="link">
207235
<Icon name="trash" />
208236
</Button>
@@ -232,7 +260,6 @@ function ConfigTargets({
232260
.getProjectActions(project_id)
233261
.open_file({ path, foreground: false });
234262
}
235-
close?.();
236263
};
237264

238265
return (
@@ -241,7 +268,10 @@ function ConfigTargets({
241268
<div style={{ flex: 1 }}>
242269
<Divider>
243270
Target Courses{" "}
244-
<Tooltip mouseEnterDelay={1} title="Open all selected targets in background tabs.">
271+
<Tooltip
272+
mouseEnterDelay={1}
273+
title="Open all selected targets in background tabs."
274+
>
245275
<a onClick={openAll}>(open all)</a>
246276
</Tooltip>
247277
</Divider>
@@ -258,7 +288,7 @@ function ConfigTargets({
258288
actions.set({ copy_config_targets, table: "settings" });
259289
}}
260290
>
261-
Clear
291+
None
262292
</Button>
263293
<Button
264294
disabled={numFalse(targets) == 0}
@@ -288,7 +318,7 @@ function getOptions(settings) {
288318
function ConfigOptions({ settings, actions, numOptions }) {
289319
const options = getOptions(settings);
290320
const v: JSX.Element[] = [];
291-
for (const option of COPY_OPTIONS) {
321+
for (const option of CONFIGURATION_GROUPS) {
292322
const { title, label, icon } = COMMANDS[option] ?? {};
293323
v.push(
294324
<Tooltip key={option} title={title} mouseEnterDelay={1}>
@@ -319,20 +349,20 @@ function ConfigOptions({ settings, actions, numOptions }) {
319349
size="small"
320350
onClick={() => {
321351
const copy_config_options = {} as CopyConfigurationOptions;
322-
for (const option of COPY_OPTIONS) {
352+
for (const option of CONFIGURATION_GROUPS) {
323353
copy_config_options[option] = false;
324354
}
325355
actions.set({ copy_config_options, table: "settings" });
326356
}}
327357
>
328-
Clear
358+
None
329359
</Button>
330360
<Button
331-
disabled={numOptions == COPY_OPTIONS.length}
361+
disabled={numOptions == CONFIGURATION_GROUPS.length}
332362
size="small"
333363
onClick={() => {
334364
const copy_config_options = {} as CopyConfigurationOptions;
335-
for (const option of COPY_OPTIONS) {
365+
for (const option of CONFIGURATION_GROUPS) {
336366
copy_config_options[option] = true;
337367
}
338368
actions.set({ copy_config_options, table: "settings" });

0 commit comments

Comments
 (0)