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/compute-servers.tsx
Views: 687
import { A } from "@cocalc/frontend/components/A";1import ComputeServer, { currentlyEditing } from "./compute-server";2import CreateComputeServer from "./create-compute-server";3import { useTypedRedux } from "@cocalc/frontend/app-framework";4import { cmp, plural } from "@cocalc/util/misc";5import { availableClouds } from "./config";6import {7Alert,8Button,9Input,10Card,11Checkbox,12Radio,13Switch,14Tooltip,15} from "antd";16import { useEffect, useState } from "react";17const { Search } = Input;18import { search_match, search_split } from "@cocalc/util/misc";19import {20SortableList,21SortableItem,22DragHandle,23} from "@cocalc/frontend/components/sortable-list";24import { webapp_client } from "@cocalc/frontend/webapp-client";25import { Icon } from "@cocalc/frontend/components";26import { STATE_TO_NUMBER } from "@cocalc/util/db-schema/compute-servers";27import {28get_local_storage,29set_local_storage,30} from "@cocalc/frontend/misc/local-storage";3132export function Docs({ style }: { style? }) {33return (34<A style={style} href="https://doc.cocalc.com/compute_server.html">35<Icon name="external-link" /> Docs36</A>37);38}3940export default function ComputeServers({ project_id }: { project_id: string }) {41const computeServers = useTypedRedux({ project_id }, "compute_servers");42const account_id = useTypedRedux("account", "account_id");43const [help, setHelp] = useState<boolean>(false);44const supported = availableClouds().length > 0;4546return (47<div style={{ paddingRight: "15px" }}>48{supported && (49<>50<Switch51checkedChildren={"Help"}52unCheckedChildren={"Help"}53style={{ float: "right" }}54checked={help}55onChange={setHelp}56/>57{help && (58<div style={{ fontSize: "12pt" }}>59<A href="https://doc.cocalc.com/compute_server.html">60Compute Servers61</A>{" "}62provide <strong>affordable GPUs</strong>,{" "}63<strong>high end VM's</strong>, <strong>root access</strong>,{" "}64<strong>Docker</strong> and <strong>Kubernetes</strong> on CoCalc.65Compute servers are virtual machines where you and your66collaborators can run Jupyter notebooks, terminals and web servers67collaboratively, with full access to your project.68<ul>69<li>70<Icon name="ubuntu" /> Full root and internet access on an71Ubuntu Linux server,72</li>73<li>74<Icon name="server" /> Dedicated GPUs, hundreds of very fast75vCPUs, and thousands of GB of RAM76</li>77<li>78{" "}79<Icon name="dns" /> Public ip address and (optional) domain80name81</li>82<li>83{" "}84<Icon name="sync" /> Files sync'd with the project85</li>86</ul>87<h3>Getting Started</h3>88<ul>89<li>Create a compute server below and start it.</li>90<li>91Once your compute server is running, select it in the upper92left of any terminal or Jupyter notebook in this project.{" "}93</li>94<li>95Compute servers stay running independently of your project, so96if you need to restart your project for any reason, that97doesn't impact running notebooks and terminals on your compute98servers.99</li>100<li>101A compute server belongs to the user who created it, and they102will be billed by the second for usage. Select "Allow103Collaborator Control" to allow project collaborators to start104and stop a compute server. Project collaborators can always105connect to running compute servers.106</li>107<li>108You can ssh to user@ at the ip address of your compute server109using any{" "}110<A href="https://doc.cocalc.com/project-settings.html#ssh-keys">111project112</A>{" "}113or{" "}114<A href="https://doc.cocalc.com/account/ssh.html">115account public ssh keys116</A>{" "}117that has access to this project (wait about 30 seconds after118you add keys). If you start a web service on any port P on119your compute server, type{" "}120<code>ssh -L P:localhost:P user@ip_address</code>121on your laptop, and you can connect to that web service on122localhost on your laptop. Also ports 80 and 443 are always123publicly visible (so no port forwarding is required). If you124connect to root@ip_address, you are root on the underlying125virtual machine outside of any Docker container; if you126connect to user@ip_address, you are the user inside the main127compute container, with full access to your chosen image --128this is the same as opening a terminal and selecting the129compute server.130</li>131</ul>132<h3>Click this Button ↓</h3>133</div>134)}135</>136)}137{supported ? (138<ComputeServerTable139computeServers={computeServers}140project_id={project_id}141account_id={account_id}142/>143) : (144<b>No Compute Server Clouds are currently enabled.</b>145)}146</div>147);148}149150function computeServerToSearch(computeServers, id) {151return JSON.stringify(computeServers.get(id)).toLowerCase();152}153154function ComputeServerTable({155computeServers: computeServers0,156project_id,157account_id,158}) {159const [computeServers, setComputeServers] = useState<any>(computeServers0);160useEffect(() => {161setComputeServers(computeServers0);162}, [computeServers0]);163164const [search, setSearch0] = useState<string>(165(get_local_storage(`${project_id}-compute-server-search`) ?? "") as string,166);167const setSearch = (value) => {168setSearch0(value);169set_local_storage(`${project_id}-compute-server-search`, value);170};171const [showDeleted, setShowDeleted] = useState<boolean>(false);172const [sortBy, setSortBy] = useState<173"id" | "title" | "custom" | "edited" | "state"174>(175(get_local_storage(`${project_id}-compute-server-sort`) ?? "custom") as any,176);177178if (!computeServers || computeServers.size == 0) {179return (180<div style={{ textAlign: "center" }}>181<CreateComputeServer182project_id={project_id}183onCreate={() => setSearch("")}184/>185</div>186);187}188const search_words = search_split(search.toLowerCase());189const ids: number[] = [];190let numDeleted = 0;191let numSkipped = 0;192for (const [id] of computeServers) {193if (currentlyEditing.id == id) {194// always include the one that is currently being edited. We wouldn't want,195// e.g., changing the title shouldn't make the editing modal vanish!196ids.push(id);197continue;198}199const isDeleted = !!computeServers.getIn([id, "deleted"]);200if (isDeleted) {201numDeleted += 1;202}203if (showDeleted != isDeleted) {204continue;205}206if (search_words.length > 0) {207if (208!search_match(computeServerToSearch(computeServers, id), search_words)209) {210numSkipped += 1;211continue;212}213}214ids.push(id);215}216ids.sort((a, b) => {217if (a == b) {218return 0;219}220const cs_a = computeServers.get(a);221const cs_b = computeServers.get(b);222if (sortBy == "custom") {223return -cmp(224cs_a.get("position") ?? cs_a.get("id"),225cs_b.get("position") ?? cs_b.get("id"),226);227} else if (sortBy == "title") {228return cmp(229cs_a.get("title")?.toLowerCase(),230cs_b.get("title")?.toLowerCase(),231);232} else if (sortBy == "id") {233// sort by id234return -cmp(cs_a.get("id"), cs_b.get("id"));235} else if (sortBy == "edited") {236return -cmp(cs_a.get("last_edited") ?? 0, cs_b.get("last_edited") ?? 0);237} else if (sortBy == "state") {238const a = cmp(239STATE_TO_NUMBER[cs_a.get("state")] ?? 100,240STATE_TO_NUMBER[cs_b.get("state")] ?? 100,241);242if (a == 0) {243return -cmp(244cs_a.get("position") ?? cs_a.get("id"),245cs_b.get("position") ?? cs_b.get("id"),246);247}248return a;249} else {250return -cmp(cs_a.get("id"), cs_b.get("id"));251}252});253254const renderItem = (id) => {255const server = computeServers.get(id).toJS();256257return (258<div style={{ display: "flex" }}>259{sortBy == "custom" && (260<div261style={{262fontSize: "20px",263color: "#888",264display: "flex",265justifyContent: "center",266flexDirection: "column",267marginRight: "5px",268}}269>270<DragHandle id={id} />271</div>272)}273<ComputeServer274server={server}275style={{ marginBottom: "10px" }}276key={`${id}`}277editable={account_id == server.account_id}278controls={{ setShowDeleted }}279/>280</div>281);282};283284const v: JSX.Element[] = [];285for (const id of ids) {286v.push(287<SortableItem key={`${id}`} id={id}>288{renderItem(id)}289</SortableItem>,290);291}292293return (294<div style={{ margin: "5px" }}>295<div style={{ margin: "15px 0", textAlign: "center" }} key="create">296<CreateComputeServer297project_id={project_id}298onCreate={() => setSearch("")}299/>300</div>301<Card>302<div style={{ marginBottom: "15px" }}>303{computeServers.size > 1 && (304<Search305allowClear306placeholder={`Filter ${computeServers.size} Compute ${plural(307computeServers.size,308"Server",309)}...`}310value={search}311onChange={(e) => setSearch(e.target.value)}312style={{ width: 300, maxWidth: "100%" }}313/>314)}315{computeServers.size > 1 && (316<span317style={{318marginLeft: "15px",319display: "inline-block",320marginTop: "5px",321float: "right",322}}323>324Sort:{" "}325<Radio.Group326buttonStyle="solid"327value={sortBy}328size="small"329onChange={(e) => {330setSortBy(e.target.value);331try {332set_local_storage(333`${project_id}-compute-server-sort`,334e.target.value,335);336} catch (_) {}337}}338>339<Tooltip title="Custom sort order with drag and drop via handle on the left">340<Radio.Button value="custom">Custom</Radio.Button>341</Tooltip>342<Tooltip title="Sort by state with most alive (e.g., 'running') being first">343<Radio.Button value="state">State</Radio.Button>344</Tooltip>345<Tooltip title="Sort by when something about compute server last changed">346<Radio.Button value="edited">Changed</Radio.Button>347</Tooltip>348<Tooltip title="Sort in alphabetical order by the title">349<Radio.Button value="title">Title</Radio.Button>350</Tooltip>351<Tooltip title="Sort by the numerical id from highest (newest) to lowest (oldest)">352<Radio.Button value="id">Id</Radio.Button>353</Tooltip>354</Radio.Group>355</span>356)}357{numDeleted > 0 && (358<Checkbox359style={{ marginLeft: "10px", marginTop: "5px" }}360checked={showDeleted}361onChange={() => setShowDeleted(!showDeleted)}362>363Deleted ({numDeleted})364</Checkbox>365)}366</div>367{numSkipped > 0 && (368<Alert369showIcon370style={{ margin: "15px auto", maxWidth: "600px" }}371type="warning"372message={373<div style={{ marginTop: "5px" }}>374Not showing {numSkipped} compute servers due to current filter.375<Button376type="text"377style={{ float: "right", marginTop: "-5px" }}378onClick={() => setSearch("")}379>380Clear381</Button>382</div>383}384/>385)}386<div387style={{ /* maxHeight: "60vh", overflow: "auto", */ width: "100%" }}388>389<SortableList390disabled={sortBy != "custom"}391items={ids}392Item={({ id }) => renderItem(id)}393onDragStop={(oldIndex, newIndex) => {394let position;395if (newIndex == ids.length - 1) {396const last = computeServers.get(ids[ids.length - 1]);397// putting it at the bottom, so subtract 1 from very bottom position398position = (last.get("position") ?? last.get("id")) - 1;399} else {400// putting it above what was at position newIndex.401if (newIndex == 0) {402// very top403const first = computeServers.get(ids[0]);404// putting it at the bottom, so subtract 1 from very bottom position405position = (first.get("position") ?? first.get("id")) + 1;406} else {407// not at the very top: between two408let x, y;409if (newIndex > oldIndex) {410x = computeServers.get(ids[newIndex]);411y = computeServers.get(ids[newIndex + 1]);412} else {413x = computeServers.get(ids[newIndex - 1]);414y = computeServers.get(ids[newIndex]);415}416417const x0 = x.get("position") ?? x.get("id");418const y0 = y.get("position") ?? y.get("id");419// TODO: yes, positions could get too close and this doesn't work, and then420// we have to globally reset them all. This is done for jupyter etc.421// not implemented here *yet*.422position = (x0 + y0) / 2;423}424}425const id = ids[oldIndex];426let cur = computeServers.get(ids[oldIndex]);427cur = cur.set("position", position);428setComputeServers(computeServers.set(ids[oldIndex], cur));429(async () => {430try {431await webapp_client.async_query({432query: {433compute_servers: {434id,435project_id,436position,437},438},439});440} catch (err) {441console.warn(err);442}443})();444}}445>446{v}447</SortableList>448</div>449</Card>450</div>451);452}453454455