Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/course/configuration/upgrades.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Upgrading quotas for all student projects67import {8Button,9Card,10Checkbox,11Divider,12Form,13Popconfirm,14Radio,15Space,16Switch,17Typography,18} from "antd";19import { delay } from "awaiting";20import { useEffect, useState } from "react";21import { FormattedMessage, useIntl } from "react-intl";2223import { alert_message } from "@cocalc/frontend/alerts";24import { CSS, redux, useActions } from "@cocalc/frontend/app-framework";25import { A, Icon, Paragraph } from "@cocalc/frontend/components";26import Next from "@cocalc/frontend/components/next";27import { labels } from "@cocalc/frontend/i18n";28import { SiteLicenseInput } from "@cocalc/frontend/site-licenses/input";29import { SiteLicensePublicInfoTable } from "@cocalc/frontend/site-licenses/site-license-public-info";30import { SiteLicenses } from "@cocalc/frontend/site-licenses/types";31import { ShowSupportLink } from "@cocalc/frontend/support";32import { COLORS } from "@cocalc/util/theme";33import { CourseActions } from "../actions";34import {35CourseSettingsRecord,36CourseStore,37DEFAULT_LICENSE_UPGRADE_HOST_PROJECT,38} from "../store";39import { SiteLicenseStrategy } from "../types";40import { ConfigurationActions } from "./actions";4142const radioStyle: CSS = {43display: "block",44whiteSpace: "normal",45fontWeight: "inherit", // this is to undo what react-bootstrap does to the labels.46} as const;4748interface Props {49name: string;50is_onprem: boolean;51is_commercial: boolean;52institute_pay?: boolean;53student_pay?: boolean;54site_license_id?: string;55site_license_strategy?: SiteLicenseStrategy;56shared_project_id?: string;57disabled?: boolean;58settings: CourseSettingsRecord;59actions: ConfigurationActions;60}6162export function StudentProjectUpgrades({63name,64is_onprem,65is_commercial,66institute_pay,67student_pay,68site_license_id,69site_license_strategy,70shared_project_id,71disabled,72settings,73actions,74}: Props) {75const intl = useIntl();7677const course_actions = useActions<CourseActions>({ name });78const [show_site_license, set_show_site_license] = useState<boolean>(false);7980function get_store(): CourseStore {81return redux.getStore(name) as any;82}8384async function add_site_license_id(license_id: string) {85course_actions.configuration.add_site_license_id(license_id);86await delay(100);87course_actions.configuration.configure_all_projects();88}8990async function remove_site_license_id(license_id: string) {91course_actions.configuration.remove_site_license_id(license_id);92await delay(100);93course_actions.configuration.configure_all_projects();94}9596function render_site_license_text() {97if (!show_site_license) return;98return (99<div>100<br />101<FormattedMessage102id="course.upgrades.site_license_text.info"103defaultMessage={`Enter a license key below to automatically apply upgrades from that104license to this course project, all student projects, and the shared105project whenever they are running. Clear the field below to stop106applying those upgrades. Upgrades from the license are only applied when107a project is started.`}108/>{" "}109{is_commercial ? (110<FormattedMessage111id="course.upgrades.site_license_text.info-commercial"112defaultMessage={`Create a {support} if you need to purchase a license key113via a purchase order.`}114values={{115support: <ShowSupportLink />,116}}117/>118) : undefined}119<SiteLicenseInput120onSave={(license_id) => {121set_show_site_license(false);122add_site_license_id(license_id);123}}124onCancel={() => {125set_show_site_license(false);126}}127/>128</div>129);130}131132function render_licenses(site_licenses: SiteLicenses): JSX.Element {133return (134<SiteLicensePublicInfoTable135site_licenses={site_licenses}136onRemove={(license_id) => {137remove_site_license_id(license_id);138}}139warn_if={(info, _) => {140const upgradeHostProject = settings.get(141"license_upgrade_host_project",142);143const n =144get_store().get_student_ids().length +145(upgradeHostProject ? 1 : 0) +146(shared_project_id ? 1 : 0);147if (info.run_limit < n) {148return (149<FormattedMessage150id="course.upgrades.render_licenses.note"151defaultMessage={`NOTE: This license can only upgrade {run_limit} simultaneous running projects,152but there are {n} projects associated to this course.`}153values={{ n, run_limit: info.run_limit }}154/>155);156}157}}158/>159);160}161162function render_site_license_strategy() {163return (164<Paragraph165style={{166margin: "0",167border: `1px solid ${COLORS.GRAY_L}`,168padding: "15px",169borderRadius: "5px",170}}171>172<FormattedMessage173id="course.upgrades.license_strategy.explanation"174defaultMessage={`<b>License strategy:</b>175Since you have multiple licenses,176there are two different ways they can be used,177depending on whether you're trying to maximize the number of covered students178or the upgrades per students:`}179/>180<br />181<Radio.Group182disabled={disabled}183style={{ marginLeft: "15px", marginTop: "15px" }}184onChange={(e) => {185course_actions.configuration.set_site_license_strategy(186e.target.value,187);188course_actions.configuration.configure_all_projects(true);189}}190value={site_license_strategy ?? "serial"}191>192<Radio value={"serial"} key={"serial"} style={radioStyle}>193<FormattedMessage194id="course.upgrades.license_strategy.radio.coverage"195defaultMessage={`<b>Maximize number of covered students:</b>196apply one license to each project associated to this course197(e.g., you bought a license to handle a few more students who were added your course).198If you have more students than license seats,199the first students to start their projects will get the upgrades.`}200/>201</Radio>202<Radio value={"parallel"} key={"parallel"} style={radioStyle}>203<FormattedMessage204id="course.upgrades.license_strategy.radio.upgrades"205defaultMessage={` <b>Maximize upgrades to each project:</b>206apply all licenses to all projects associated to this course207(e.g., you bought a license to increase the RAM or CPU for all students).`}208/>209</Radio>210</Radio.Group>211<Divider type="horizontal" />212<FormattedMessage213id="course.upgrades.license_strategy.redistribute"214defaultMessage={`<Button>Redistribute licenses</Button> – e.g. useful if a license expired`}215values={{216Button: (c) => (217<Button218onClick={() =>219course_actions.configuration.configure_all_projects(true)220}221size="small"222>223<Icon name="arrows" /> {c}224</Button>225),226}}227/>228</Paragraph>229);230}231232function render_current_licenses() {233if (!site_license_id) return;234const licenses = site_license_id.split(",");235236const site_licenses: SiteLicenses = licenses.reduce((acc, v) => {237acc[v] = null; // we have no info about them yet238return acc;239}, {});240241return (242<div style={{ margin: "15px 0" }}>243<FormattedMessage244id="course.upgades.current_licenses.info"245defaultMessage={`This project and all student projects will be upgraded using the246following247<b>{n} {n, select, 1 {license} other {licenses}}</b>,248unless it is expired or in use by too many projects:`}249values={{ n: licenses.length }}250/>251<br />252<div style={{ margin: "15px 0", padding: "0" }}>253{render_licenses(site_licenses)}254</div>255{licenses.length > 1 && render_site_license_strategy()}256</div>257);258}259260function render_remove_all_licenses() {261return (262<Popconfirm263title={intl.formatMessage({264id: "course.upgrades.remove_all_licenses.title",265defaultMessage: "Remove all licenses from all student projects?",266})}267onConfirm={async () => {268try {269await course_actions.student_projects.remove_all_project_licenses();270alert_message({271type: "info",272message: intl.formatMessage({273id: "course.upgrades.remove_all_licenses.success",274defaultMessage:275"Successfully removed all licenses from student projects.",276}),277});278} catch (err) {279alert_message({ type: "error", message: `${err}` });280}281}}282>283<Button style={{ marginTop: "15px" }}>284<FormattedMessage285id="course.upgrades.remove_all_licenses"286defaultMessage={"Remove licenses from student projects..."}287/>288</Button>289</Popconfirm>290);291}292293function render_site_license() {294const n = !!site_license_id ? site_license_id.split(",").length : 0;295return (296<div>297{render_current_licenses()}298<div>299<Button300onClick={() => set_show_site_license(true)}301disabled={show_site_license}302>303<Icon name="key" />{" "}304<FormattedMessage305id="course.upgades.site_license.upgrade-button.label"306defaultMessage={`{n, select,3070 {Upgrade using a license key}308other {Add another license key (more students or better upgrades)}}`}309values={{ n }}310/>311...312</Button>313{render_site_license_text()}314</div>315<Space>316{is_commercial && (317<div style={{ marginTop: "15px" }}>318<Next319href={"store/site-license"}320query={{321user: "academic",322period: "range",323run_limit: (get_store()?.num_students() ?? 0) + 2,324member: true,325uptime: "short",326cpu: 1,327ram: 2,328disk: 3,329title: settings.get("title") ?? "",330description: settings.get("description") ?? "",331}}332>333<Button>334<FormattedMessage335id="course.upgrades.site_license.buy-button.label"336defaultMessage={"Buy a license..."}337/>338</Button>339</Next>340</div>341)}342{n == 0 && render_remove_all_licenses()}343</Space>344<div>345<ToggleUpgradingHostProject actions={actions} settings={settings} />346</div>347</div>348);349}350351function handle_institute_pay_checkbox(e): void {352course_actions.configuration.set_pay_choice("institute", e.target.checked);353}354355function render_checkbox() {356return (357<Checkbox358checked={!!institute_pay}359onChange={handle_institute_pay_checkbox}360>361<FormattedMessage362id="course.upgrades.checkbox-institute-pays"363defaultMessage={"You or your institute will pay for this course"}364/>365</Checkbox>366);367}368369function render_details() {370return (371<div style={{ marginTop: "15px" }}>372{render_site_license()}373<hr />374<div style={{ color: COLORS.GRAY_M }}>375<p>376<FormattedMessage377id="course.upgrades.details"378defaultMessage={`Add or remove upgrades to student projects associated to this course,379adding to what is provided for free and what students may have purchased.380<A>Help...</A>`}381values={{382A: (c) => (383<A href="https://doc.cocalc.com/teaching-create-course.html#option-2-teacher-or-institution-pays-for-upgradespay">384{c}385</A>386),387}}388description={"Students in an university online course."}389/>390</p>391</div>392</div>393);394}395396function render_onprem(): JSX.Element {397return <div>{render_site_license()}</div>;398}399400function render_title() {401if (is_onprem) {402return (403<div>404<FormattedMessage405id="course.upgrades.onprem.title"406defaultMessage={"Upgrade Student Projects"}407/>408</div>409);410} else {411return (412<div>413<Icon name="dashboard" />{" "}414<FormattedMessage415id="course.upgrades.prod.title"416defaultMessage={"Upgrade all Student Projects (Institute Pays)"}417/>418</div>419);420}421}422423function render_body(): JSX.Element {424if (is_onprem) {425return render_onprem();426} else {427return (428<>429{render_checkbox()}430{institute_pay ? render_details() : undefined}431</>432);433}434}435436return (437<Card438style={{439marginTop: "20px",440background:441is_onprem || student_pay || institute_pay ? undefined : "#fcf8e3",442}}443title={render_title()}444>445{render_body()}446</Card>447);448}449450interface ToggleUpgradingHostProjectProps {451actions: ConfigurationActions;452settings: CourseSettingsRecord;453}454455const ToggleUpgradingHostProject = ({456actions,457settings,458}: ToggleUpgradingHostProjectProps) => {459const intl = useIntl();460const [needSave, setNeedSave] = useState<boolean>(false);461const upgradeHostProject = settings.get("license_upgrade_host_project");462const upgrade = upgradeHostProject ?? DEFAULT_LICENSE_UPGRADE_HOST_PROJECT;463const [nextVal, setNextVal] = useState<boolean>(upgrade);464465useEffect(() => {466setNeedSave(nextVal != upgrade);467}, [nextVal, upgrade]);468469const label = intl.formatMessage({470id: "course.upgrades.toggle-host.label",471defaultMessage: "Upgrade instructor project:",472});473474function toggle() {475return (476<Form layout="inline">477<Form.Item label={label} style={{ marginBottom: 0 }}>478<Switch checked={nextVal} onChange={(val) => setNextVal(val)} />479</Form.Item>480<Form.Item>481<Button482disabled={!needSave}483type={needSave ? "primary" : undefined}484onClick={() => actions.set_license_upgrade_host_project(nextVal)}485>486{intl.formatMessage(labels.save)}487</Button>488</Form.Item>489</Form>490);491}492493return (494<>495<hr />496{toggle()}497<Typography.Paragraph498ellipsis={{ expandable: true, rows: 1, symbol: "more" }}499>500{intl.formatMessage({501id: "course.upgrades.toggle-host.info",502defaultMessage: `If enabled, this instructor project is upgraded using all configured course license(s).503Otherwise, explictly add your license to the instructor project.504Disabling this options does <i>not</i> remove licenses from the instructor project.`,505})}506</Typography.Paragraph>507</>508);509};510511512