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/compute/public-templates.tsx
Views: 687
import { Select, Spin, Tag, Tooltip } from "antd";1import { useEffect, useState } from "react";2import { getTemplates } from "@cocalc/frontend/compute/api";3import type { ConfigurationTemplate } from "@cocalc/util/compute/templates";4import type { HyperstackConfiguration } from "@cocalc/util/db-schema/compute-servers";5import { CLOUDS_BY_NAME } from "@cocalc/util/compute/cloud/clouds";6import { avatar_fontcolor } from "@cocalc/frontend/account/avatar/font-color";7import { cmp, currency, search_match } from "@cocalc/util/misc";8import HyperstackSpecs from "@cocalc/frontend/compute/cloud/hyperstack/specs";9import GoogleCloudSpecs from "@cocalc/frontend/compute/cloud/google-cloud/specs";10import { RenderImage } from "@cocalc/frontend/compute/images";11import { filterOption } from "@cocalc/frontend/compute/util";12import DisplayCloud from "./display-cloud";13import { Icon } from "@cocalc/frontend/components/icon";1415const { CheckableTag } = Tag;1617const TAGS = {18GPU: {19label: (20<>21<Icon name="gpu" /> GPU22</>23),24search: hasGPU,25desc: "that have a GPU",26group: 0,27},28H100: {29label: (30<>31<Icon name="nvidia" /> H10032</>33),34search: (template) =>35template.configuration.flavor_name?.toLowerCase().includes("h100"),36desc: "that have a high end NVIDIA H100 GPU",37group: 0,38},39A100: {40label: (41<>42<Icon name="nvidia" /> A10043</>44),45search: (template) =>46template.configuration.flavor_name?.toLowerCase().includes("a100") ||47template.configuration.acceleratorType?.toLowerCase().includes("a100"),48desc: "that have a high end NVIDIA A100 GPU",49group: 0,50},51L40: {52label: (53<>54<Icon name="nvidia" /> L4055</>56),57search: (template) =>58template.configuration.flavor_name?.toLowerCase().includes("l40"),59desc: "that have a midrange NVIDIA L40 GPU",60group: 0,61},62RTX: {63label: (64<>65<Icon name="nvidia" /> RTX66</>67),68search: (template) =>69template.configuration.flavor_name?.toLowerCase().includes("rtx"),70desc: "that have a midrange NVIDIA RTX-4000/5000/6000 GPU",71group: 0,72},7374L4: {75label: (76<>77<Icon name="nvidia" /> L478</>79),80search: (template) =>81template.configuration.acceleratorType82?.toLowerCase()83.includes("nvidia-l4"),84desc: "that have a midrange NVIDIA L4 GPU",85group: 0,86},87T4: {88label: (89<>90<Icon name="nvidia" /> T491</>92),93search: (template) =>94template.configuration.acceleratorType95?.toLowerCase()96.includes("tesla-t4"),97desc: "that have a budget NVIDIA T4 GPU",98group: 0,99},100CPU: {101label: (102<>103<Icon name="microchip" /> CPU104</>105),106search: (template) => !hasGPU(template),107desc: "that have no GPU's",108group: 0,109},110Python: {111label: (112<>113<Icon name="python" /> Python114</>115),116search: ({ configuration }) => {117const im = configuration.image.toLowerCase();118return (119im.includes("python") || im.includes("anaconda") || im.includes("colab")120);121},122desc: "with a Python oriented image",123group: 1,124},125SageMath: {126label: (127<>128<Icon name="sagemath" /> Sage129</>130),131search: ({ configuration }) => {132const im = configuration.image.toLowerCase();133return im.includes("sage") || im.includes("anaconda");134},135desc: "with a Julia oriented image",136group: 1,137},138Julia: {139label: (140<>141<Icon name="julia" /> Julia142</>143),144search: ({ configuration }) => {145const im = configuration.image.toLowerCase();146return im.includes("julia") || im.includes("anaconda");147},148desc: "with a Julia oriented image",149group: 1,150},151R: {152label: (153<>154<Icon name="r" /> R155</>156),157search: ({ configuration }) => {158const im = configuration.image.toLowerCase();159return im.includes("rstat") || im.includes("colab");160},161desc: "with an R Statistics oriented image",162group: 1,163},164PyTorch: {165label: (166<>167<Icon name="pytorch" /> PyTorch168</>169),170search: ({ configuration }) => {171const im = configuration.image.toLowerCase();172return (173im.includes("torch") || im.includes("colab") || im.includes("conda")174);175},176desc: "with a PyTorch capable image",177group: 1,178},179Tensorflow: {180label: (181<>182<Icon name="tensorflow" /> Tensorflow183</>184),185search: ({ configuration }) => {186const im = configuration.image.toLowerCase();187return (188im.includes("tensorflow") ||189im.includes("colab") ||190im.includes("conda")191);192},193desc: "with a Tensorflow oriented image",194group: 1,195},196HPC: {197label: (198<>199<Icon name="cube" /> HPC/Fortran200</>201),202search: ({ configuration }) => {203const im = configuration.image.toLowerCase();204return im == "hpc";205},206desc: "with an HPC/Fortran oriented image",207group: 1,208},209Ollama: {210label: (211<>212<Icon name="magic" /> Ollama213</>214),215search: ({ configuration }) => {216const im = configuration.image.toLowerCase();217return im.includes("openwebui");218},219desc: "with an Open WebUI / Ollama AI oriented image",220group: 1,221},222Google: {223label: <DisplayCloud cloud="google-cloud" height={18} />,224search: ({ configuration }) => configuration.cloud == "google-cloud",225group: 2,226desc: "in Google Cloud",227},228Hyperstack: {229label: <DisplayCloud cloud="hyperstack" height={18} />,230search: ({ configuration }) => configuration.cloud == "hyperstack",231group: 2,232desc: "in Hyperstack Cloud",233},234} as const;235236export default function PublicTemplates({237style,238setId,239defaultId,240disabled,241defaultOpen,242placement,243getPopupContainer,244}: {245style?;246setId: (number) => void;247defaultId?: number;248disabled?: boolean;249defaultOpen?: boolean;250placement?;251getPopupContainer?;252}) {253const [loading, setLoading] = useState<boolean>(false);254const [templates, setTemplates] = useState<255(ConfigurationTemplate | { search: string })[] | null256>(null);257const [data, setData] = useState<any>(null);258const [options, setOptions] = useState<any[]>([]);259const [visibleTags, setVisibleTags] = useState<Set<string>>(new Set());260const [filterTags, setFilterTags] = useState<Set<string>>(new Set());261const [selectOpen, setSelectOpen] = useState<boolean>(!!defaultOpen);262const [value, setValue0] = useState<number | undefined>(defaultId);263const setValue = (n: number) => {264setValue0(n);265setId(n);266};267268useEffect(() => {269(async () => {270try {271setLoading(true);272const { templates, data } = await getTemplates();273if (templates == null || templates.length == 0) {274setTemplates(null);275setData(null);276setOptions([]);277return;278}279setTemplates(templates);280setData(data);281const options = getOptions(templates, data);282const tags = new Set<string>();283for (const tag in TAGS) {284if (matchingOptions(options, tag).length > 0) {285tags.add(tag);286}287}288setVisibleTags(tags);289} finally {290setLoading(false);291}292})();293}, []);294295useEffect(() => {296if (templates == null) {297return;298}299let options = getOptions(templates, data);300if (filterTags.size > 0) {301for (const tag of filterTags) {302options = matchingOptions(options, tag);303}304// we also sort by price when there is a filter (otherwise not)305options.sort((a, b) =>306cmp(a.template.cost_per_hour.running, b.template.cost_per_hour.running),307);308}309setOptions(options);310}, [filterTags, templates, data]);311312if (loading) {313return (314<div style={{ maxWidth: "1200px", margin: "15px auto", ...style }}>315Loading Templates... <Spin />316</div>317);318}319320if (templates == null || templates?.length == 0) {321// not loaded or no configured templates right now.322return null;323}324325let group = 0;326327return (328<div style={{ maxWidth: "1200px", margin: "15px auto", ...style }}>329<div style={{ display: "flex" }}>330<div331style={{332fontWeight: "bold",333fontSize: "13pt",334flex: 0.1,335color: "#666",336display: "flex",337justifyContent: "center",338flexDirection: "column",339whiteSpace: "nowrap",340paddingLeft: "15px",341}}342>343Templates:344</div>345<div346style={{347flex: 1,348textAlign: "center",349marginBottom: "5px",350fontWeight: "normal",351border: "1px solid lightgrey",352borderRadius: "5px",353marginLeft: "15px",354background: "#fffeee",355padding: "10px",356}}357>358{Object.keys(TAGS)359.filter((tag) => visibleTags.has(tag))360.map((name) => {361const t = (362<Tooltip363mouseEnterDelay={1}364key={name}365title={366TAGS[name].tip ?? (367<>Only show templates {TAGS[name].desc}.</>368)369}370>371{TAGS[name].group != group && <br />}372<CheckableTag373key={name}374style={{ cursor: "pointer", fontSize: "12pt" }}375checked={filterTags.has(name)}376onChange={(checked) => {377let v = Array.from(filterTags);378if (checked) {379v.push(name);380v = v.filter(381(x) => x == name || TAGS[x].group != TAGS[name].group,382);383} else {384v = v.filter((x) => x != name);385}386setFilterTags(new Set(v));387setSelectOpen(v.length > 0);388}}389>390{TAGS[name].label ?? name}391</CheckableTag>392</Tooltip>393);394group = TAGS[name].group;395return t;396})}397</div>398<div style={{ flex: 0.1 }}></div>399</div>400<Select401allowClear402open={selectOpen}403defaultOpen={defaultOpen}404placement={placement}405getPopupContainer={getPopupContainer}406disabled={disabled}407value={value}408onChange={setValue}409options={options}410style={{411width: "100%",412height: "auto",413}}414placeholder={415<div style={{ color: "#666" }}>416Use filters above or type here to find a template, then modify it...417</div>418}419showSearch420optionFilterProp="children"421filterOption={filterOption}422onDropdownVisibleChange={setSelectOpen}423/>424</div>425);426}427428function TemplateLabel({ template, data }) {429const { title, color, cloud, cost_per_hour } = template;430const cost = (431<div style={{ fontSize: "13pt" }}>432{currency(cost_per_hour.running)}/hour433</div>434);435let specs;436if (template.cloud == "hyperstack") {437specs = (438<HyperstackSpecs439{...(template.configuration as HyperstackConfiguration)}440priceData={data.hyperstackPriceData}441/>442);443} else if (template.cloud == "google-cloud") {444specs = (445<GoogleCloudSpecs446configuration={template.configuration}447priceData={data.googleCloudPriceData}448IMAGES={data.images}449/>450);451} else {452specs = null;453}454return (455<div456style={{457lineHeight: "normal",458borderWidth: "0.5px 10px",459borderStyle: "solid",460borderColor: color,461borderRadius: "5px",462padding: "10px",463overflow: "auto",464margin: "5px 10px",465}}466>467<div style={{ display: "flex", margin: "0 15px" }}>468<div style={{ flex: 1, textAlign: "center" }}>{cost}</div>469<div470style={{471flex: 1,472background: color ?? "#fff",473color: avatar_fontcolor(color ?? "#fff"),474padding: "2.5px 5px",475overflow: "auto",476}}477>478{title}479</div>480<div style={{ flex: 1 }}>481<div style={{ float: "right" }}>482<RenderImage483configuration={template.configuration}484IMAGES={data.images}485/>486</div>487</div>488<div style={{ flex: 1 }}>489<div style={{ width: "120px", float: "right" }}>490<img src={CLOUDS_BY_NAME[cloud]?.image} alt={cloud} />491</div>492</div>493</div>494<div495style={{496whiteSpace: "nowrap",497lineHeight: "normal",498marginTop: "5px",499textAlign: "center",500maxHeight: "1.2em",501textOverflow: "ellipsis",502color: "#666",503}}504>505{specs}506</div>507</div>508);509}510511function hasGPU(template) {512if (template.configuration.cloud == "hyperstack") {513return !template.configuration.flavor_name.includes("cpu");514} else if (template.configuration.cloud == "google-cloud") {515return !!template.configuration.acceleratorCount;516} else {517return JSON.stringify(template).includes("gpu");518}519}520521function getOptions(templates, data) {522return templates.map((template) => {523return {524template,525value: template.id,526label: <TemplateLabel template={template} data={data} />,527search: JSON.stringify(template).toLowerCase(),528};529});530}531532function matchingOptions(options, tag) {533const f = TAGS[tag]?.search;534if (!f) {535return options;536}537if (typeof f == "function") {538return options.filter(({ template }) => f(template));539} else {540return options.filter(({ search }) => search_match(search, f));541}542}543544545