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/share/configure-public-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 { useEffect, useState } from "react";
7
import { Alert, Divider, Radio, Input, Select, Space } from "antd";
8
import useDatabase from "lib/hooks/database";
9
import useCustomize from "lib/use-customize";
10
import Loading from "./loading";
11
import { LICENSES } from "@cocalc/frontend/share/licenses";
12
import SaveButton from "components/misc/save-button";
13
import EditRow from "components/misc/edit-row";
14
import A from "components/misc/A";
15
import SelectSiteLicense from "components/misc/select-site-license";
16
import { Icon } from "@cocalc/frontend/components/icon";
17
import LaTeX from "components/landing/latex";
18
import {
19
SHARE_AUTHENTICATED_EXPLANATION,
20
SHARE_AUTHENTICATED_ICON,
21
SHARE_FLAGS,
22
} from "@cocalc/util/consts/ui";
23
24
const { Option } = Select;
25
26
interface Props {
27
id: string;
28
project_id: string;
29
path: string;
30
}
31
32
const QUERY = {
33
name: null,
34
description: null,
35
disabled: null,
36
unlisted: null,
37
authenticated: null,
38
license: null,
39
compute_image: null,
40
};
41
42
interface Info {
43
name?: string;
44
description?: string;
45
disabled?: boolean;
46
unlisted?: boolean;
47
authenticated?: boolean;
48
license?: string;
49
compute_image?: string;
50
site_license_id?: string;
51
}
52
53
function get_visibility(edited) {
54
if (edited.disabled) return "private";
55
if (edited.unlisted) return "unlisted";
56
if (edited.authenticated) return "authenticated";
57
return "listed";
58
}
59
60
export default function ConfigurePublicPath({ id, project_id, path }: Props) {
61
const publicPaths = useDatabase({
62
public_paths: { ...QUERY, id, project_id, path },
63
});
64
const siteLicense = useDatabase({
65
public_paths_site_license_id: {
66
site_license_id: null,
67
id,
68
project_id,
69
path,
70
},
71
});
72
const { onCoCalcCom } = useCustomize();
73
const [loaded, setLoaded] = useState<boolean>(false);
74
const [edited, setEdited] = useState<Info>({});
75
const [original, setOriginal] = useState<Info>({});
76
const [error, setError] = useState<string>("");
77
78
// After loading finishes, either editor or error is set.
79
useEffect(() => {
80
if (publicPaths.loading || siteLicense.loading) return;
81
if (publicPaths.error) {
82
setError(publicPaths.error);
83
return;
84
}
85
if (siteLicense.error) {
86
setError(siteLicense.error);
87
return;
88
}
89
const { site_license_id } = siteLicense.value.public_paths_site_license_id;
90
const { public_paths } = publicPaths.value;
91
const x = { ...public_paths, site_license_id };
92
setEdited(x);
93
setOriginal(x);
94
setLoaded(true);
95
}, [publicPaths.loading, siteLicense.loading]);
96
97
if (!loaded) {
98
return <Loading delay={0.2} />;
99
}
100
101
// cheap to compute, so we compute every time.
102
const visibility = get_visibility(edited);
103
// we don't show "authenticated" on cocalc.com, unless it is set to it
104
const showAuthenticated = !onCoCalcCom || edited.authenticated;
105
106
const save =
107
edited == null || original == null ? null : (
108
<SaveButton
109
edited={edited}
110
original={original}
111
setOriginal={setOriginal}
112
table="public_paths"
113
/>
114
);
115
116
return (
117
<div
118
style={{
119
width: "100%",
120
border: "1px solid #eee",
121
padding: "15px",
122
marginTop: "15px",
123
}}
124
>
125
{error && <Alert type="error" message={error} showIcon />}
126
{save}
127
<Divider>How you are sharing "{path}"</Divider>
128
<Space direction="vertical" style={{ width: "100%" }}>
129
<EditRow
130
label="Describe what you are sharing"
131
description={
132
<>
133
Use relevant keywords, inspire curiosity by providing just enough
134
information to explain what this is about, and keep your
135
description to about two lines. Use Markdown and <LaTeX />. You
136
can change this at any time.
137
</>
138
}
139
>
140
<Input.TextArea
141
style={{ width: "100%" }}
142
value={edited.description}
143
onChange={(e) =>
144
setEdited({ ...edited, description: e.target.value })
145
}
146
autoSize={{ minRows: 2, maxRows: 6 }}
147
/>
148
</EditRow>
149
<EditRow
150
label="Choose a name for a nicer URL"
151
description="An optional name can provide a much nicer and more memorable URL. You must also name your project (in project settings) and the owner of the project to get a nice URL. (WARNING: Changing this once it is set can break links, since automatic redirection is not implemented.)"
152
>
153
<Input
154
style={{ maxWidth: "100%", width: "30em" }}
155
value={edited.name}
156
onChange={(e) => setEdited({ ...edited, name: e.target.value })}
157
/>
158
</EditRow>
159
<EditRow
160
label={
161
showAuthenticated
162
? "Listed, Unlisted, Authenticated or Private?"
163
: "Listed, Unlisted or Private?"
164
}
165
description="You make files or directories public to the world, either indexed by
166
search engines (listed), only visible with the link (unlisted), or only those who are authenticated.
167
Public files are automatically copied to the public server within about 30 seconds
168
after you explicitly edit them. You can also set a site license for unlisted public shares."
169
>
170
<Space direction="vertical">
171
<Radio.Group
172
value={visibility}
173
onChange={(e) => {
174
switch (e.target.value) {
175
case "listed":
176
setEdited({ ...edited, ...SHARE_FLAGS.LISTED });
177
break;
178
case "unlisted":
179
setEdited({ ...edited, ...SHARE_FLAGS.UNLISTED });
180
break;
181
case "authenticated":
182
setEdited({ ...edited, ...SHARE_FLAGS.AUTHENTICATED });
183
break;
184
case "private":
185
setEdited({ ...edited, ...SHARE_FLAGS.DISABLED, name: "" });
186
break;
187
}
188
}}
189
>
190
<Space direction="vertical">
191
<Radio value={"listed"}>
192
<Icon name="eye" /> <em>Public (listed): </em> anybody can
193
find this via search.
194
</Radio>
195
<Radio value={"unlisted"}>
196
<Icon name="eye-slash" /> <em>Public (unlisted):</em> only
197
people with the link can view this.
198
</Radio>
199
{showAuthenticated && (
200
<Radio value={"authenticated"}>
201
<Icon name={SHARE_AUTHENTICATED_ICON} />{" "}
202
<em>Authenticated:</em> {SHARE_AUTHENTICATED_EXPLANATION}.
203
</Radio>
204
)}
205
<Radio value={"private"}>
206
<Icon name="lock" /> <em>Private:</em> only collaborators on
207
the project can view this.
208
</Radio>
209
</Space>
210
</Radio.Group>
211
</Space>
212
</EditRow>
213
{visibility == "unlisted" && (
214
<EditRow
215
label="Upgrade with a site license?"
216
description={
217
<>
218
For unlisted shares, you can select a site license that you
219
manage, and anybody who edits a copy of this share will have
220
this site license applied to their project. You can track and
221
remove usage of this license in the{" "}
222
<A>license management page</A> (coming soon).
223
</>
224
}
225
>
226
<SelectSiteLicense
227
defaultLicenseId={original.site_license_id}
228
onChange={(site_license_id) => {
229
setEdited({ ...edited, site_license_id });
230
}}
231
/>
232
</EditRow>
233
)}
234
<EditRow
235
label="Permission"
236
description={
237
<>
238
An optional{" "}
239
<A href="https://opensource.org/licenses">open source license</A>{" "}
240
tells people how they may use what you are sharing.
241
</>
242
}
243
>
244
<License
245
license={edited.license}
246
onChange={(license) => setEdited({ ...edited, license })}
247
/>
248
</EditRow>
249
{/*TODO Image: {edited.compute_image} */}
250
</Space>
251
{save}
252
</div>
253
);
254
}
255
256
function License({ license, onChange }) {
257
const options: JSX.Element[] = [];
258
for (const value in LICENSES) {
259
options.push(
260
<Option key={value} value={value}>
261
{LICENSES[value]}
262
</Option>
263
);
264
}
265
return (
266
<Select
267
showSearch
268
value={license}
269
style={{ width: "100%" }}
270
placeholder="Select an open source license"
271
optionFilterProp="children"
272
onChange={onChange}
273
filterOption={(input, option) =>
274
option
275
? `${option.children}`.toLowerCase().includes(input.toLowerCase())
276
: false
277
}
278
>
279
{options}
280
</Select>
281
);
282
}
283
284