CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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