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/next/components/path/path.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Avatar as AntdAvatar,8Button,9Divider,10Space,11Tooltip,12} from "antd";13import Link from "next/link";14import { useRouter } from "next/router";15import { join } from "path";16import { useEffect, useState } from "react";1718import { Icon } from "@cocalc/frontend/components/icon";19import {20SHARE_AUTHENTICATED_EXPLANATION,21SHARE_AUTHENTICATED_ICON,22} from "@cocalc/util/consts/ui";23import InPlaceSignInOrUp from "components/auth/in-place-sign-in-or-up";24import { Tagline } from "components/landing/tagline";25import A from "components/misc/A";26import Badge from "components/misc/badge";27import SanitizedMarkdown from "components/misc/sanitized-markdown";28import { Layout } from "components/share/layout";29import License from "components/share/license";30import LinkedPath from "components/share/linked-path";31import Loading from "components/share/loading";32import PathActions from "components/share/path-actions";33import PathContents from "components/share/path-contents";34import ProjectLink from "components/share/project-link";35import Avatar from "components/share/proxy/avatar";36import apiPost from "lib/api/post";37import type { CustomizeType } from "lib/customize";38import useCounter from "lib/share/counter";39import { Customize } from "lib/share/customize";40import type { PathContents as PathContentsType } from "lib/share/get-contents";41import { getTitle } from "lib/share/util";4243import { SocialMediaShareLinks } from "components/landing/social-media-share-links";4445export interface PublicPathProps {46id: string;47path: string;48url: string;49project_id: string;50projectTitle?: string;51relativePath?: string;52description?: string;53counter?: number;54compute_image?: string;55license?: string;56contents?: PathContentsType;57error?: string;58customize: CustomizeType;59disabled?: boolean;60has_site_license?: boolean;61unlisted?: boolean;62authenticated?: boolean;63stars?: number;64isStarred?: boolean;65githubOrg?: string; // if given, this is being mirrored from this github org66githubRepo?: string; // if given, mirrored from this github repo.67projectAvatarImage?: string; // optional 320x320 image representing the project from which this was shared68// Do a redirect to here; this is due to names versus id and is needed when69// visiting this by following a link from within the share server that70// doesn't use the names. See https://github.com/sagemathinc/cocalc/issues/611571redirect?: string;72jupyter_api: boolean;73created: string | null; // ISO 8601 string74last_edited: string | null; // ISO 8601 string75ogUrl?: string; // Open Graph URL for social media sharing76ogImage?: string; // Open Graph image for social media sharing77}7879export default function PublicPath({80id,81path,82url,83project_id,84projectTitle,85relativePath = "",86description,87counter,88compute_image,89license,90contents,91error,92customize,93disabled,94has_site_license,95unlisted,96authenticated,97stars = 0,98isStarred: isStarred0,99githubOrg,100githubRepo,101projectAvatarImage,102redirect,103jupyter_api,104ogUrl,105}: PublicPathProps) {106useCounter(id);107const [numStars, setNumStars] = useState<number>(stars);108109const [isStarred, setIsStarred] = useState<boolean | null | undefined>(110isStarred0 ?? null,111);112useEffect(() => {113setIsStarred(isStarred0);114}, [isStarred0]);115116const [signingUp, setSigningUp] = useState<boolean>(false);117const router = useRouter();118const [invalidRedirect, setInvalidRedirect] = useState<boolean>(false);119120useEffect(() => {121if (redirect) {122// User can in theory pass in an arbitrary redirect, which could probably be dangerous (e.g., to an external123// spam/hack site!?). So we only automatically redirect to the SAME site we're on right now.124if (redirect) {125const site = siteName(redirect);126if (!site) {127// no site specified -- path relative to our own site128router.replace(redirect);129} else if (site == siteName(location.href)) {130// site specified and it is our own site.131router.replace(redirect);132} else {133// user can manually inspect url and click134setInvalidRedirect(true);135}136}137}138}, [redirect]);139140if (id == null || (redirect && !invalidRedirect)) {141return (142<div style={{ margin: "30px", textAlign: "center" }}>143<Loading style={{ fontSize: "30px" }} />144</div>145);146}147148function visibility_explanation() {149if (disabled) {150return (151<>152<Icon name="lock" /> Private (only visible to collaborators on the153project)154</>155);156}157if (unlisted) {158return (159<>160<Icon name="eye-slash" /> Unlisted (only visible to those who know the161link)162</>163);164}165if (authenticated) {166return (167<>168<Icon name={SHARE_AUTHENTICATED_ICON} /> Authenticated (169{SHARE_AUTHENTICATED_EXPLANATION})170</>171);172}173}174175function visibility() {176if (unlisted || disabled || authenticated) {177return (178<div>179<b>Visibility:</b> {visibility_explanation()}180</div>181);182}183}184185async function star() {186setIsStarred(true);187setNumStars(numStars + 1);188// Actually do the api call after changing state, so it is189// maximally snappy. Also, being absolutely certain that star/unstar190// actually worked is not important.191await apiPost("/public-paths/star", { id });192}193194async function unstar() {195setIsStarred(false);196setNumStars(numStars - 1);197await apiPost("/public-paths/unstar", { id });198}199200function renderStar() {201const badge = (202<Badge203count={numStars}204style={{205marginLeft: "10px",206marginTop: "-2.5px",207}}208/>209);210if (isStarred == null) {211// not signed in ==> isStarred is null or undefined.212return (213<Button214onClick={() => {215setSigningUp(!signingUp);216}}217title={"Sign in to star"}218>219<Icon name="star" /> Star {badge}220</Button>221);222}223// Signed in so isStarred is true or false.224let btn;225if (isStarred == true) {226btn = (227<Button onClick={unstar}>228<Icon name="star-filled" style={{ color: "#eac54f" }} /> Starred{" "}229{badge}230</Button>231);232} else {233btn = (234<Button onClick={star}>235<Icon name="star" /> Star {badge}236</Button>237);238}239return (240<div>241<A href="/stars" style={{ marginRight: "10px" }}>242Your stars...243</A>244{btn}245</div>246);247}248249function renderProjectLink() {250if (githubOrg && githubRepo) {251return (252<Tooltip253title="Go to the top level of the repository."254placement="right"255>256<b>257<Icon name="home" /> GitHub Repository:{" "}258</b>259<A href={`/github/${githubOrg}/${githubRepo}`}>260{githubOrg}/{githubRepo}261</A>262<br />263</Tooltip>264);265}266if (url) {267let name, target;268const i = url.indexOf("/");269if (url.startsWith("gist")) {270target = `https://gist.github.com/${url.slice(i + 1)}`;271name = "GitHub Gist";272} else {273target = "https://" + url.slice(i + 1);274name = "URL";275}276// NOTE: it could conceivable only be http:// display will work, but this277// link will be wrong. I'm not going to worry about that.278return (279<Tooltip280placement="right"281title={`This file is hosted at ${target}. Click to open in a new tab.`}282>283<b>284<Icon name="external-link" /> {name}:{" "}285</b>286<A href={target}>{target}</A>287<br />288</Tooltip>289);290}291return (292<div>293<b>Project:</b>{" "}294<ProjectLink project_id={project_id} title={projectTitle} />295<br />296</div>297);298}299300function renderPathLink() {301if (githubRepo) {302const segments = url.split("/");303return (304<Tooltip305placement="right"306title="This is hosted on GitHub. Click to open GitHub in a new tab."307>308<b>309<Icon name="external-link" /> Path:{" "}310</b>311<A href={`https://github.com/${join(...segments.slice(1))}`}>312{segments.length > 3313? join(...segments.slice(3))314: join(...segments.slice(1))}315</A>316<br />317</Tooltip>318);319}320321if (url) return;322323return (324<div>325<b>Path: </b>326<LinkedPath327path={path}328relativePath={relativePath}329id={id}330isDir={contents?.isdir}331/>332<br />333</div>334);335}336337return (338<Customize value={customize}>339<Layout340title={getTitle({ path, relativePath })}341top={342projectAvatarImage ? (343<AntdAvatar344shape="square"345size={160}346icon={347<img348src={projectAvatarImage}349alt={`Avatar for ${projectTitle}.`}350/>351}352style={{ float: "left", margin: "20px" }}353/>354) : undefined355}356>357{githubOrg && (358<Avatar359size={96}360name={githubOrg}361style={{ float: "right", marginLeft: "15px" }}362/>363)}364<div>365<Tagline366value={customize.indexTagline}367style={{ marginTop: "-15px", padding: "5px" }}368/>369{invalidRedirect && (370<Alert371type="warning"372message={373<>374<Icon name="external-link" /> External Redirect375</>376}377description={378<div>379The author has configured a redirect to:{" "}380<div style={{ fontSize: "13pt", textAlign: "center" }}>381<A href={redirect}>{redirect}</A>382</div>383</div>384}385style={{ margin: "15px 0" }}386/>387)}388<PathActions389id={id}390path={path}391url={url}392relativePath={relativePath}393isDir={contents?.isdir}394exclude={new Set(["hosted"])}395project_id={project_id}396image={compute_image}397description={description}398has_site_license={has_site_license}399/>400<Space401style={{402float: "right",403justifyContent: "flex-end",404}}405direction="vertical"406>407<div style={{ float: "right" }}>{renderStar()}</div>408</Space>409{signingUp && (410<Alert411closable412onClick={() => setSigningUp(false)}413style={{ margin: "0 auto", maxWidth: "400px" }}414type="warning"415message={416<InPlaceSignInOrUp417title="Star Shared Files"418why="to star this"419onSuccess={() => {420star();421setSigningUp(false);422router.reload();423}}424/>425}426/>427)}428{description?.trim() && (429<SanitizedMarkdown430style={431{ marginBottom: "-1em" } /* -1em to undo it being a paragraph */432}433value={description}434/>435)}436437{renderProjectLink()}438{renderPathLink()}439{counter && (440<>441<b>Views:</b> <Badge count={counter} />442<br />443</>444)}445{license && (446<>447<b>License:</b> <License license={license} />448<br />449</>450)}451{visibility()}452{compute_image && (453<>454<b>Image:</b> {compute_image}455<br />456</>457)}458</div>459{ogUrl && (460<SocialMediaShareLinks461title={getTitle({ path, relativePath })}462url={ogUrl}463showText464/>465)}466<Divider />467{error != null && (468<Alert469showIcon470type="error"471style={{ maxWidth: "700px", margin: "30px auto" }}472message="Error loading file"473description={474<div>475There was a problem loading{" "}476{relativePath ? relativePath : "this file"} in{" "}477<Link href={`/share/public_paths/${id}`}>{path}.</Link>478<br />479<br />480{error}481</div>482}483/>484)}485{contents != null && (486<PathContents487id={id}488relativePath={relativePath}489path={path}490jupyter_api={jupyter_api}491{...contents}492/>493)}494</Layout>495</Customize>496);497}498499function siteName(url) {500const i = url.indexOf("://");501if (i == -1) {502return "";503}504const j = url.indexOf("/", i + 3);505if (j == -1) {506return url;507}508return url.slice(0, j);509}510511512