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/licenses/how-used.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Checkbox, Input, Popover, Table } from "antd";
7
import { useEffect, useMemo, useState } from "react";
8
9
import { capitalize, cmp, search_match, search_split } from "@cocalc/util/misc";
10
import Avatar from "components/account/avatar";
11
import { Paragraph, Title } from "components/misc";
12
import A from "components/misc/A";
13
import Copyable from "components/misc/copyable";
14
import Loading from "components/share/loading";
15
import apiPost from "lib/api/post";
16
import editURL from "lib/share/edit-url";
17
import { useRouter } from "next/router";
18
import { Details as License } from "./license";
19
import { LastEdited } from "./licensed-projects";
20
import { Quota, quotaColumn } from "./managed";
21
import SelectLicense from "./select-license";
22
23
function TitleId({ title, project_id, collaborators, account_id, label }) {
24
return (
25
<div style={{ wordWrap: "break-word", wordBreak: "break-word" }}>
26
{collaborators.includes(account_id) ? (
27
<A href={editURL({ project_id, type: "collaborator" })} external>
28
{title}
29
</A>
30
) : (
31
title
32
)}
33
{label && (
34
<>
35
<br />
36
Project Id:
37
</>
38
)}
39
<Copyable value={project_id} size="small" />
40
</div>
41
);
42
}
43
44
function Collaborators({ collaborators }) {
45
return (
46
<>
47
{collaborators.map((account_id) => (
48
<Avatar
49
key={account_id}
50
account_id={account_id}
51
size={24}
52
style={{ marginRight: "2.5px" }}
53
/>
54
))}
55
</>
56
);
57
}
58
59
function State({ state }) {
60
return <>{capitalize(state)}</>;
61
}
62
63
export default function HowLicenseUsed({ account_id }) {
64
const router = useRouter();
65
const [license, setLicense] = useState<string>(
66
`${router.query.license_id ?? ""}`
67
);
68
const [search, setSearch] = useState<string>("");
69
const [error, setError] = useState<string>("");
70
let [projects, setProjects] = useState<object[]>([]);
71
const [loading, setLoading] = useState<boolean>(false);
72
const [excludeMe, setExcludeMe] = useState<boolean>(false);
73
74
const columns = useMemo(() => {
75
return [
76
{
77
responsive: ["xs"],
78
render: (_, project) => (
79
<div>
80
<TitleId {...project} account_id={account_id} label />
81
<div>
82
Last Edited: <LastEdited {...project} />
83
</div>
84
<div>
85
State: <State {...project} />
86
</div>
87
<div>
88
Collaborators: <Collaborators {...project} />
89
</div>
90
<div>
91
{"Quota in use: "}
92
<Quota {...project} />
93
</div>
94
</div>
95
),
96
},
97
{
98
responsive: ["sm"],
99
title: (
100
<Popover
101
placement="bottom"
102
title="Project"
103
content={
104
<div style={{ maxWidth: "75ex" }}>
105
This is the title and id of the project. If you are a
106
collaborator on this project, then you can click the title to
107
open the project.
108
</div>
109
}
110
>
111
Project
112
</Popover>
113
),
114
width: "30%",
115
render: (_, project) => (
116
<TitleId {...project} account_id={account_id} />
117
),
118
sorter: { compare: (a, b) => cmp(a.title, b.title) },
119
},
120
{
121
responsive: ["sm"],
122
title: "Last Edited",
123
render: (_, project) => <LastEdited {...project} />,
124
sorter: { compare: (a, b) => cmp(a.last_edited, b.last_edited) },
125
},
126
{
127
responsive: ["sm"],
128
title: (
129
<Popover
130
title="Collaborators"
131
content={
132
<div style={{ maxWidth: "75ex" }}>
133
These are the collaborators on this project. You are not
134
necessarily included in this list, since this license can be
135
applied to any project by somebody who knows the license code.
136
Click the "Exclude me" checkbox to see only projects that you
137
are <b>not</b> a collaborator on.
138
</div>
139
}
140
>
141
Collaborators
142
<div style={{ fontWeight: 300 }}>
143
<Checkbox
144
onChange={(e) => setExcludeMe(e.target.checked)}
145
checked={excludeMe}
146
>
147
Exclude me
148
</Checkbox>
149
</div>
150
</Popover>
151
),
152
render: (_, project) => <Collaborators {...project} />,
153
},
154
{
155
responsive: ["sm"],
156
title: "State",
157
dataIndex: "state",
158
key: "state",
159
sorter: { compare: (a, b) => cmp(a.state, b.state) },
160
render: (_, project) => <State {...project} />,
161
},
162
quotaColumn,
163
];
164
}, [account_id, excludeMe]);
165
166
async function load(license_id) {
167
setLicense(license_id);
168
setError("");
169
if (license_id) {
170
setLoading(true);
171
setProjects([]);
172
try {
173
setProjects(
174
await apiPost("/licenses/get-projects-with-license", {
175
license_id,
176
})
177
);
178
} catch (err) {
179
setError(err.message);
180
} finally {
181
setLoading(false);
182
}
183
}
184
}
185
186
useEffect(() => {
187
// initial license load (e.g., from query param)
188
if (license) {
189
load(license);
190
}
191
}, []);
192
193
return (
194
<div style={{ width: "100%", overflowX: "auto", minHeight: "50vh" }}>
195
<Title level={2}>How a License You Manage is Being Used</Title>
196
<Paragraph>
197
Select a license you manage to see how it is being used. You can see{" "}
198
<i>all</i> projects that have this license applied to them (even if you
199
are not a collaborator on them!), remove licenses from projects, and
200
view analytics about how the license has been used over time to better
201
inform your decision making.
202
</Paragraph>
203
<div style={{ margin: "15px 0", width: "100%", textAlign: "center" }}>
204
<SelectLicense
205
disabled={loading}
206
onSelect={(license_id) => {
207
router.push({
208
pathname: router.asPath.split("?")[0],
209
query: { license_id },
210
});
211
load(license_id);
212
}}
213
license={license}
214
style={{ width: "100%", maxWidth: "90ex" }}
215
/>
216
</div>
217
{license && error && <Alert type="error" message={error} />}
218
{license && loading && (
219
<Loading style={{ fontSize: "16pt", margin: "auto" }} />
220
)}
221
<div
222
style={{
223
border: "1px solid lightgrey",
224
borderRadius: "5px",
225
padding: "15px",
226
backgroundColor: "#fafafa",
227
width: "100%",
228
maxWidth: "90ex",
229
margin: "auto",
230
}}
231
>
232
{license ? (
233
<License license_id={license} />
234
) : (
235
<div style={{ textAlign: "center", fontSize: "13pt" }}>
236
Select a license above.
237
</div>
238
)}
239
</div>
240
{license && !loading && projects.length > 1 && (
241
<div style={{ margin: "15px 0", maxWidth: "50ex" }}>
242
<Input.Search
243
placeholder="Search project titles..."
244
allowClear
245
onChange={(e) => setSearch(e.target.value)}
246
style={{ width: "100%" }}
247
/>
248
</div>
249
)}
250
{license && !loading && (
251
<Table
252
columns={columns as any}
253
dataSource={doSearch(projects, search, excludeMe, account_id)}
254
rowKey={"project_id"}
255
style={{ marginTop: "15px" }}
256
pagination={{ hideOnSinglePage: true, pageSize: 100 }}
257
/>
258
)}
259
</div>
260
);
261
}
262
263
function doSearch(
264
data: object[],
265
search: string,
266
excludeMe: boolean,
267
account_id: string
268
): object[] {
269
const v = search_split(search.toLowerCase().trim());
270
const w: object[] = [];
271
for (const x of data) {
272
if (excludeMe && x["collaborators"]?.includes(account_id)) continue;
273
if (x["search"] == null) {
274
x["search"] = `${x["title"] ?? ""} ${x["id"]}`.toLowerCase();
275
}
276
if (search_match(x["search"], v)) {
277
w.push(x);
278
}
279
}
280
return w;
281
}
282
283