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/custom-software/selector.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Col, Row, Alert, Button, Divider, List, Radio } from "antd";6import { join } from "path";7import {8CSS,9React,10redux,11useMemo,12useState,13useTypedRedux,14} from "@cocalc/frontend/app-framework";15import {16A,17Gap,18Icon,19Markdown,20Paragraph,21SearchInput,22} from "@cocalc/frontend/components";23import {24CompanyName,25HelpEmailLink,26SiteName,27} from "@cocalc/frontend/customize";28import { appBasePath } from "@cocalc/frontend/customize/app-base-path";29import { ComputeImageSelector } from "@cocalc/frontend/project/settings/compute-image-selector";30import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";31import { unreachable } from "@cocalc/util/misc";32import { COLORS } from "@cocalc/util/theme";33import { ComputeImage, ComputeImageTypes, ComputeImages } from "./init";34import {35CUSTOM_SOFTWARE_HELP_URL,36compute_image2basename,37custom_image_name,38is_custom_image,39} from "./util";4041const BINDER_URL = "https://mybinder.readthedocs.io/en/latest/";4243const CS_LIST_STYLE: CSS = {44height: "250px",45overflowX: "hidden" as "hidden",46overflowY: "scroll" as "scroll",47border: `1px solid ${COLORS.GRAY_LL}`,48borderRadius: "5px",49marginBottom: "0px",50} as const;5152const ENTRIES_ITEM_STYLE: CSS = {53width: "100%",54margin: "2px 0px",55padding: "5px",56border: "none",57textAlign: "left" as "left",58} as const;5960export interface SoftwareEnvironmentState {61image_selected?: string;62title_text?: string;63image_type?: ComputeImageTypes;64}6566// this is used in create-project and course/configuration/actions67// this derives the proper image name from the image type & image selection of SoftwareEnvironmentState68export async function derive_project_img_name(69custom_software: SoftwareEnvironmentState,70): Promise<string> {71const { image_type, image_selected } = custom_software;72const dflt_software_img = await redux73.getStore("customize")74.getDefaultComputeImage();75if (image_selected == null || image_type == null) {76return dflt_software_img;77}78switch (image_type) {79case "custom":80return custom_image_name(image_selected);81case "default":82case "standard":83return image_selected;84default:85unreachable(image_type);86return dflt_software_img; // make TS happy87}88}8990interface Props {91onChange: (obj: SoftwareEnvironmentState) => void;92default_image?: string; // which one to initialize state to93showTitle?: boolean; // default true94}9596// this is a selector for the software environment of a project97export const SoftwareEnvironment: React.FC<Props> = (props: Props) => {98const { onChange, default_image, showTitle = true } = props;99const images: ComputeImages | undefined = useTypedRedux(100"compute_images",101"images",102);103104const customize_kucalc = useTypedRedux("customize", "kucalc");105const onCoCalcCom = customize_kucalc === KUCALC_COCALC_COM;106const customize_software = useTypedRedux("customize", "software");107const [dflt_software_img, software_images] = useMemo(108() => [109customize_software.get("default"),110customize_software.get("environments"),111],112[customize_software],113);114115const haveSoftwareImages: boolean = useMemo(116() => (customize_software.get("environments")?.size ?? 0) > 0,117[customize_software],118);119120const [search_img, set_search_img] = useState<string>("");121const [image_selected, set_image_selected] = useState<string | undefined>(122undefined,123);124const set_title_text = useState<string | undefined>(undefined)[1];125const [image_type, set_image_type] = useState<ComputeImageTypes>("default");126127function set_state(128image_selected: string | undefined,129title_text: string | undefined,130image_type: ComputeImageTypes,131): void {132set_image_selected(image_selected);133set_title_text(title_text);134set_image_type(image_type);135onChange({ image_selected, title_text, image_type });136}137138// initialize selection, if there is a default image set139React.useEffect(() => {140if (default_image == null || default_image === dflt_software_img) {141// do nothing, that's the initial state already!142} else if (is_custom_image(default_image)) {143if (images == null) return;144const id = compute_image2basename(default_image);145const img: ComputeImage | undefined = images.get(id);146if (img == null) {147// ignore, user has to select from scratch148} else {149set_state(id, img.get("display", ""), "custom");150}151} else {152// must be standard image153const img = software_images.get(default_image);154const display = img != null ? (img.get("title") ?? "") : "";155set_state(default_image, display, "standard");156}157}, []);158159function render_custom_image_entries() {160if (images == null) return;161162const search_hit = (() => {163if (search_img.length > 0) {164return (img: ComputeImage) =>165img.get("search_str", "").indexOf(search_img.toLowerCase()) >= 0;166} else {167return (_img: ComputeImage) => true;168}169})();170171const entries: JSX.Element[] = images172.filter((img) => img.get("type", "") === "custom")173.filter(search_hit)174.sortBy((img) => img.get("display", "").toLowerCase())175.entrySeq()176.map((e) => {177const [id, img] = e;178const display = img.get("display", "");179return (180<List.Item181key={id}182onClick={() => set_state(id, display, image_type)}183style={{184...ENTRIES_ITEM_STYLE,185...(image_selected === id186? { background: "#337ab7", color: "white" }187: undefined),188}}189>190{display}191</List.Item>192);193})194.toArray();195196if (entries.length > 0) {197return <List style={CS_LIST_STYLE}>{entries}</List>;198} else {199if (search_img.length > 0) {200return <div>No search hits.</div>;201} else {202return <div>No custom software available</div>;203}204}205}206207function search(val: string): void {208set_search_img(val);209set_state(undefined, undefined, image_type);210}211212function render_custom_images() {213if (image_type !== "custom") return;214215return (216<>217<div style={{ display: "flex" }}>218<SearchInput219placeholder={"Search…"}220autoFocus={false}221value={search_img}222on_escape={() => set_search_img("")}223on_change={search}224style={{ flex: "1" }}225/>226</div>227{render_custom_image_entries()}228</>229);230}231232function render_custom_images_info() {233if (image_type !== "custom") return;234235return (236<>237<div style={{ color: COLORS.GRAY, margin: "15px 0" }}>238Contact us to add more or give feedback:{" "}239<HelpEmailLink color={COLORS.GRAY} />.240</div>241<Alert242type="info"243banner244message={245<>246The selected <em>custom</em> software environment stays with the247project. Create a new project to work in a different software248environment. You can always{" "}249<A250href={251"https://doc.cocalc.com/project-files.html#file-actions-on-one-file"252}253>254copy files between projects255</A>{" "}256as well.257</>258}259/>260</>261);262}263264function render_selected_custom_image_info() {265if (image_type !== "custom" || image_selected == null || images == null) {266return;267}268269const id: string = image_selected;270const data = images.get(id);271if (data == null) {272// we have a serious problem273console.warn(`compute_image data missing for '${id}'`);274return;275}276// some fields are derived in the "Table" when the data comes in277const img: ComputeImage = data;278const disp = img.get("display");279const desc = img.get("desc", "");280const url = img.get("url");281const src = img.get("src");282const disp_tag = img.get("display_tag");283284const render_source = () => {285if (src == null || src.length == 0) return;286return (287<div style={{ marginTop: "5px" }}>288Source: <code>{src}</code>289</div>290);291};292293const render_url = () => {294if (url == null || url.length == 0) return;295return (296<div style={{ marginTop: "5px" }}>297<a href={url} target={"_blank"} rel={"noopener"}>298<Icon name="external-link" /> Website299</a>300</div>301);302};303304return (305<>306<h3 style={{ marginTop: "5px" }}>{disp}</h3>307<div style={{ marginTop: "5px" }}>308Image ID: <code>{disp_tag}</code>309</div>310<div311style={{ marginTop: "10px", overflowY: "auto", maxHeight: "200px" }}312>313<Markdown value={desc} className={"cc-custom-image-desc"} />314</div>315{render_source()}316{render_url()}317</>318);319}320321function render_onprem() {322const selected = image_selected ?? dflt_software_img;323return (324<>325<Paragraph>326Select the software enviornment. Either go with the default327environment, or select one of the more specialized ones. Whatever you328pick, you can change it later in Project Settings → Control → Software329Environment at any time.330</Paragraph>331<Paragraph>332<ComputeImageSelector333size={"middle"}334selected_image={selected}335layout={"horizontal"}336onSelect={(img) => {337const display = software_images.get(img)?.get("title");338set_state(img, display, "standard");339}}340/>341</Paragraph>342<Paragraph>343{selected !== dflt_software_img ? (344<Alert345type="info"346banner347message={348<>349You've selected a non-standard image:{" "}350<Button351size="small"352type="link"353onClick={() => {354set_state(dflt_software_img, undefined, "standard");355}}356>357Reset358</Button>359</>360}361/>362) : undefined}363</Paragraph>364</>365);366}367368function render_default_explanation(): JSX.Element {369if (onCoCalcCom) {370return (371<>372<b>Default</b>: large repository of software, well tested – maintained373by <CompanyName />, running <SiteName />.{" "}374<a375href={join(appBasePath, "doc/software.html")}376target={"_blank"}377rel={"noopener"}378>379More info...380</a>381</>382);383} else {384const dflt_img = software_images.get(dflt_software_img);385const descr = dflt_img?.get("descr") ?? "large repository of software";386const t = dflt_img?.get("title");387const title = t ? `${t}: ${descr}` : descr;388return (389<>390<b>Standard</b>: {title}391</>392);393}394}395396function render_default() {397return (398<Radio399checked={image_type === "default"}400id={"default-compute-image"}401onChange={() => {402set_state(undefined, undefined, "default");403}}404>405{render_default_explanation()}406</Radio>407);408}409410function render_standard_explanation(): JSX.Element {411if (onCoCalcCom) {412return (413<>414<b>Standard</b>: upcoming and archived versions of the "Default"415software environment.416</>417);418} else {419return (420<>421<b>Specialized</b>: alternative software environments for specific422purposes.423</>424);425}426}427428function render_standard_modify_later(): JSX.Element {429return (430<Alert431type="info"432banner433message={434<>435The selected <em>standard</em> software environment can be changed436in Project Settings → Control at any time.437</>438}439/>440);441}442443function render_standard() {444if (!haveSoftwareImages) {445return;446}447return (448<Radio449checked={image_type === "standard"}450id={"default-compute-image"}451onChange={() => {452set_state(undefined, undefined, "standard");453}}454>455{render_standard_explanation()}456</Radio>457);458}459460function render_custom() {461if (customize_kucalc !== KUCALC_COCALC_COM) {462return null;463}464465if (images == null || images.size == 0) {466return "There are no customized software environments available.";467} else {468return (469<Radio470checked={image_type === "custom"}471id={"custom-compute-image"}472onChange={() => {473set_state(undefined, undefined, "custom");474}}475>476<b>Custom</b>477<sup>478<em>beta</em>479</sup>480: 3rd party software environments, e.g.{" "}481<a href={BINDER_URL} target={"_blank"} rel={"noopener"}>482Binder483</a>484.{" "}485<a href={CUSTOM_SOFTWARE_HELP_URL} target={"_blank"}>486More info...487</a>488</Radio>489);490}491}492493function render_standard_image_selector() {494if (image_type !== "standard") return;495496return (497<Col sm={24}>498<ComputeImageSelector499selected_image={image_selected ?? dflt_software_img}500layout={"horizontal"}501onSelect={(img) => {502const display = software_images.get(img)?.get("title");503set_state(img, display, "standard");504}}505/>506<Gap />507{render_standard_modify_later()}508</Col>509);510}511512function render_type_selection() {513return (514<>515{showTitle ? <div>Software environment</div> : undefined}516517{onCoCalcCom ? (518<div>519<div style={{ marginBottom: "5px" }}>{render_default()}</div>520<div style={{ marginBottom: "5px" }}>{render_standard()}</div>521<div style={{ marginBottom: "5px" }}>{render_custom()}</div>522</div>523) : (524render_onprem()525)}526</>527);528}529530function render_divider() {531if (image_type === "default") return;532return (533<Divider orientation="left" plain>534Configuration535</Divider>536);537}538539return (540<Row>541<Col sm={24} style={{ marginTop: "10px" }}>542{render_type_selection()}543</Col>544545{onCoCalcCom ? (546<>547{render_divider()}548{render_standard_image_selector()}549<Col sm={12}>{render_custom_images()}</Col>550<Col sm={12}>{render_selected_custom_image_info()}</Col>551<Col sm={24}>{render_custom_images_info()}</Col>552</>553) : undefined}554</Row>555);556};557558559