Path: blob/master/src/packages/frontend/compute/select-image.tsx
5844 views
import { Icon, isIconName, Markdown } from "@cocalc/frontend/components";1import { A } from "@cocalc/frontend/components/A";2import type {3Architecture,4Configuration,5GoogleCloudImages,6Images,7State,8} from "@cocalc/util/db-schema/compute-servers";9import { makeValidGoogleName } from "@cocalc/util/db-schema/compute-servers";10import { field_cmp, trunc } from "@cocalc/util/misc";11import { Alert, Checkbox, Select, Spin } from "antd";12import { CSSProperties, useEffect, useMemo, useState } from "react";13import Advanced from "./advanced";14import { RenderImage } from "./images";15import { useImages } from "./images-hook";16import SelectVersion from "./select-version";1718interface Props {19setConfig;20configuration: Configuration;21disabled?: boolean;22state?: State;23style?: CSSProperties;24// if explicitly set, only gpu images shown when25// gpu true, and only non-gpu when false.26gpu: boolean;27// if googleImages is set, use this to restrict list of images to only28// what is actually available in non-advanced view, and to enhance the29// view otherwise (explicitly saying images aren't actually available)30googleImages?: GoogleCloudImages;31arch: Architecture;32// if specified, only show images with dockerSizeGb set and <= maxDockerSizeGb33// Ignored if advanced is selected34maxDockerSizeGb?: number;35// show a warning if dockerSizeGb is bigger than this:36warnBigGb?: number;37}3839export default function SelectImage({40setConfig,41configuration,42disabled,43state = "deprovisioned",44style,45gpu,46googleImages,47arch,48maxDockerSizeGb,49warnBigGb,50}: Props) {51const [advanced, setAdvanced] = useState<boolean>(false);52const [IMAGES, ImagesError] = useImages();53const [dockerSizeGb, setDockerSizeGb] = useState<number | undefined>(54undefined,55);56const [value, setValue] = useState<string | undefined>(configuration.image);57useEffect(() => {58setValue(configuration.image);59}, [configuration.image]);6061const options = useMemo(() => {62if (IMAGES == null || typeof IMAGES == "string") {63return [];64}65return getOptions({66IMAGES,67googleImages,68gpu,69advanced,70value,71selectedTag: configuration.tag,72arch,73maxDockerSizeGb,74});75}, [IMAGES, gpu, advanced, value, configuration.tag]);7677if (IMAGES == null) {78return <Spin />;79}80if (ImagesError != null) {81return ImagesError;82}83const filterOption = (input: string, option?: { search: string }) =>84(option?.search ?? "").includes(input.toLowerCase());8586return (87<div>88<Advanced89advanced={advanced}90setAdvanced={setAdvanced}91style={{ float: "right", marginTop: "10px" }}92title={93"Show possibly untested, old, missing, or broken images and versions."94}95/>96<Select97size="large"98disabled={disabled || state != "deprovisioned"}99placeholder="Select compute server image..."100defaultOpen={!value && state == "deprovisioned"}101value={value}102style={{ width: "500px", ...style }}103options={options}104onChange={(val) => {105setValue(val);106const x = {107image: val,108tag: null,109};110for (const option of options) {111if (option.value == val) {112setDockerSizeGb(option.dockerSizeGb);113break;114}115}116setConfig(x);117}}118showSearch119filterOption={filterOption}120/>121{advanced && IMAGES != null && typeof IMAGES != "string" && value && (122<SelectVersion123style={{ margin: "10px 0" }}124disabled={disabled || state != "deprovisioned"}125image={value}126IMAGES={IMAGES}127setConfig={setConfig}128configuration={configuration}129/>130)}131{warnBigGb && (dockerSizeGb ?? 1) > warnBigGb && (132<Alert133style={{ margin: "15px 0" }}134type="warning"135message={<h4>Large Image Warning</h4>}136description={137<>138The compute server will take{" "}139<b>up to {Math.ceil((dockerSizeGb ?? 1) / 3)} extra minutes</b> to140start the first time, because a {dockerSizeGb} GB Docker image141must be pulled and decompressed. Please be patient!142<br />143<br />144<Checkbox>145I understand that initial startup will take at least{" "}146{Math.ceil((dockerSizeGb ?? 1) / 3)} extra minutes147</Checkbox>148</>149}150/>151)}152</div>153);154}155156function getOptions({157IMAGES,158advanced,159googleImages,160gpu,161value,162selectedTag,163arch,164maxDockerSizeGb,165}: {166IMAGES: Images;167advanced?: boolean;168gpu?: boolean;169value?: string;170selectedTag?: string;171googleImages?: GoogleCloudImages;172arch: Architecture;173maxDockerSizeGb?: number;174}) {175const options: {176key: string;177tag: string;178priority: number;179value: string;180search: string;181label: React.JSX.Element;182dockerSizeGb?: number;183}[] = [];184for (const name in IMAGES) {185const image = IMAGES[name];186let { label, icon, versions, priority = 0, dockerSizeGb } = image;187if (image.system) {188continue;189}190if (image.disabled && !advanced) {191continue;192}193if (gpu != null && gpu != image.gpu) {194continue;195}196if (!advanced && maxDockerSizeGb != null) {197if (dockerSizeGb == null || dockerSizeGb > maxDockerSizeGb) {198continue;199}200}201if (!advanced) {202// restrict to only tested versions.203versions = versions.filter((x) => x.tested);204205if (googleImages != null) {206const x = googleImages[name];207// on google cloud, so make sure image is built and tested208versions = versions.filter(209(y) =>210x?.[`${makeValidGoogleName(y.tag)}-${makeValidGoogleName(arch)}`]211?.tested,212);213}214}215if (versions.length == 0) {216// no available versions, so no point in showing this option217continue;218}219let tag;220let versionLabel: string | undefined = undefined;221if (selectedTag && name == value) {222tag = selectedTag;223for (const x of versions) {224if (x.tag == tag) {225versionLabel = x.label ?? tag;226break;227}228}229} else {230tag = versions[versions.length - 1]?.tag;231versionLabel = versions[versions.length - 1]?.label ?? tag;232}233234let extra = "";235if (advanced && googleImages != null) {236const img =237googleImages[name]?.[238`${makeValidGoogleName(tag)}-${makeValidGoogleName(arch)}`239];240if (!img) {241extra = " (no image)";242} else {243const tested = img?.tested;244if (!tested) {245extra = " (not tested)";246}247}248}249if (dockerSizeGb) {250extra += ` - ${dockerSizeGb} GB`;251}252253options.push({254key: name,255value: name,256priority,257search: label?.toLowerCase() ?? "",258tag,259dockerSizeGb,260label: (261<div style={{ fontSize: "12pt" }}>262<div style={{ float: "right" }}>{versionLabel}</div>263{isIconName(icon) && (264<Icon name={icon} style={{ marginRight: "5px" }} />265)}{" "}266{label}267{image.disabled && <> (disabled)</>}268{extra}269</div>270),271});272}273options.sort(field_cmp("priority")).reverse();274return options;275}276277export function ImageLinks({ image, style }: { image; style? }) {278const [IMAGES, ImagesError] = useImages();279if (IMAGES == null) {280return <Spin />;281}282if (typeof IMAGES == "string") {283return ImagesError;284}285const data = IMAGES[image];286if (data == null) {287return null;288}289return (290<div291style={{292display: "flex",293flexDirection: "column",294marginTop: "10px",295...style,296}}297>298{data.videos != null && data.videos.length > 0 && (299<A style={{ flex: 1 }} href={data.videos[0]}>300<Icon name="youtube" style={{ color: "red" }} /> YouTube301</A>302)}303{data.tutorials != null && data.tutorials.length > 0 && (304<A style={{ flex: 1 }} href={data.tutorials[0]}>305<Icon name="graduation-cap" /> Tutorial306</A>307)}308<A style={{ flex: 1 }} href={data.source}>309<Icon name="github" /> GitHub310</A>311{!!data.url && (312<A style={{ flex: 1 }} href={data.url}>313<Icon name="external-link" /> {trunc(data.label, 10)}314</A>315)}316{!!data.package && (317<A style={{ flex: 1 }} href={packageNameToUrl(data.package)}>318<Icon name="docker" /> DockerHub319</A>320)}321</div>322);323}324325// this is a heuristic but is probably right in many cases, and326// right now the only case is n<=1, where it is right.327function packageNameToUrl(name: string): string {328const n = name.split("/").length - 1;329if (n <= 1) {330return `https://hub.docker.com/r/${name}`;331} else {332// e.g., us-docker.pkg.dev/colab-images/public/runtime333return `https://${name}`;334}335}336337export function DisplayImage({338configuration,339style,340}: {341configuration: { image: string };342style?;343}) {344const [IMAGES, ImagesError] = useImages();345if (ImagesError != null) {346return ImagesError;347}348return (349<RenderImage configuration={configuration} style={style} IMAGES={IMAGES} />350);351}352353export function ImageDescription({354configuration,355}: {356configuration: { image: string };357}) {358const [IMAGES, ImagesError] = useImages();359if (IMAGES == null) {360return <Spin />;361}362if (typeof IMAGES == "string") {363return ImagesError;364}365return (366<Alert367style={{ padding: "7.5px 15px", marginTop: "10px" }}368type="info"369description={370<Markdown371value={IMAGES[configuration?.image ?? ""]?.description ?? ""}372/>373}374/>375);376}377378379