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/license.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, Popover, Progress } from "antd";
7
import { CSSProperties } from "react";
8
9
import { describe_quota } from "@cocalc/util/licenses/describe-quota";
10
import { capitalize, stripeAmount } from "@cocalc/util/misc";
11
import { Paragraph } from "components/misc";
12
import A from "components/misc/A";
13
import Copyable from "components/misc/copyable";
14
import Timestamp from "components/misc/timestamp";
15
import Loading from "components/share/loading";
16
import useAPI from "lib/hooks/api";
17
import { EditableDescription, EditableTitle } from "./editable-license";
18
19
interface Props {
20
license_id: string;
21
contrib?: { [project_id: string]: object };
22
style?: CSSProperties;
23
}
24
25
export default function License({ license_id, style }: Props) {
26
// TODO: do something with contrib
27
return (
28
<Popover
29
content={() => <Details license_id={license_id} />}
30
title={license_id}
31
mouseEnterDelay={0.5}
32
>
33
<A
34
style={{ cursor: "pointer", fontFamily: "monospace", ...style }}
35
href={`/licenses/how-used?license_id=${license_id}`}
36
>
37
{license_id}
38
</A>
39
</Popover>
40
);
41
}
42
43
/*
44
{
45
"id": "cad2fe88-29d7-4f4e-987c-a3ae6143a25b",
46
"title": "different business license for specific time",
47
"description": "",
48
"expires": 1642799517638,
49
"activates": 1640121117638,
50
"last_used": null,
51
"managers": [
52
"93620c6e-324a-4217-a60e-2ac436953174"
53
],
54
"upgrades": null,
55
"quota": {
56
"cpu": 1,
57
"ram": 1,
58
"disk": 1,
59
"user": "business",
60
"member": true,
61
"dedicated_cpu": 0,
62
"dedicated_ram": 0,
63
"always_running": true
64
},
65
"run_limit": 3,
66
is_manager:true,
67
number_running:2
68
}
69
*/
70
71
interface DetailsProps {
72
license_id: string; // the license id
73
style?: CSSProperties; // style for the outer div
74
condensed?: boolean; // if true, only show a brief run_limit x quota description
75
type?: "cost" | "all";
76
plan?: { amount: number; currency: string };
77
}
78
79
export function Details(props: DetailsProps) {
80
const { license_id, style, type = "all", condensed = false, plan } = props;
81
const { result, error } = useAPI("licenses/get-license", { license_id }, 3); // 3s cache
82
if (error) {
83
return <Alert type="error" message={error} />;
84
}
85
if (!result) {
86
return <Loading />;
87
}
88
89
function title() {
90
if (condensed) return;
91
return (
92
(result.title || result.is_manager) && (
93
<Paragraph style={{ fontWeight: "bold", fontSize: "13pt" }}>
94
{result.is_manager ? (
95
<EditableTitle license_id={license_id} title={result.title} />
96
) : (
97
"Title: " + result.title
98
)}
99
</Paragraph>
100
)
101
);
102
}
103
104
function description() {
105
if (condensed) return;
106
return (
107
(result.description || result.is_manager) && (
108
<Paragraph>
109
{result.is_manager ? (
110
<EditableDescription
111
license_id={license_id}
112
description={result.description}
113
/>
114
) : (
115
"Description: " + result.description
116
)}
117
</Paragraph>
118
)
119
);
120
}
121
122
function managers() {
123
if (condensed) return;
124
return (
125
result.managers != null && (
126
<Paragraph>You are a manager of this license.</Paragraph>
127
)
128
);
129
}
130
131
function date() {
132
if (condensed) return;
133
return (
134
<Paragraph>
135
<DateRange {...result} />
136
</Paragraph>
137
);
138
}
139
140
function lastUsed() {
141
if (condensed) return;
142
return (
143
result.last_used != null && (
144
<Paragraph>
145
Last used: <Timestamp epoch={result.last_used} />
146
</Paragraph>
147
)
148
);
149
}
150
151
function quota() {
152
return (
153
<Paragraph>
154
{condensed ? `${result.run_limit ?? 1} x` : "Quota:"}{" "}
155
<Quota quota={result.quota} upgrades={result.upgrades} />
156
</Paragraph>
157
);
158
}
159
160
function runLimit() {
161
if (condensed) return null;
162
return (
163
result.run_limit != null && (
164
<Paragraph style={{ width: "100%", display: "flex" }}>
165
Run limit: {result.run_limit}
166
{result.number_running != null
167
? `; Currently running: ${result.number_running}`
168
: ""}
169
{result.run_limit && result.number_running != null && (
170
<Progress
171
style={{
172
marginLeft: "15px",
173
flex: 1,
174
}}
175
percent={Math.round(
176
(result.number_running / result.run_limit) * 100
177
)}
178
/>
179
)}
180
</Paragraph>
181
)
182
);
183
}
184
185
function copyId() {
186
if (condensed) return;
187
return (
188
result.is_manager && (
189
<Copyable label="ID:" value={license_id} style={{ marginTop: "5px" }} />
190
)
191
);
192
}
193
194
// this is a special case (fallback), used in billing/subscriptions. It loads additional license data
195
// and combines that with the amount and currency, already known from the plan it looks at.
196
if (type === "cost") {
197
if (plan == null) {
198
return <></>;
199
}
200
return (
201
<div style={style}>
202
Cost:{" "}
203
{stripeAmount(
204
plan.amount,
205
plan.currency,
206
result.info?.purchased.quantity ?? 1
207
)}
208
</div>
209
);
210
}
211
212
return (
213
<div style={style}>
214
{title()}
215
{description()}
216
{managers()}
217
{date()}
218
{lastUsed()}
219
{quota()}
220
{runLimit()}
221
{copyId()}
222
</div>
223
);
224
}
225
226
export function Quota({ quota, upgrades }: { quota?: any; upgrades?: any }) {
227
if (quota == null) {
228
if (upgrades == null) {
229
return <></>;
230
} else {
231
// These are very old, and we just do a little bit to display
232
// something valid.
233
quota = {
234
cpu: upgrades.cores,
235
ram: upgrades.memory / 1000,
236
};
237
}
238
}
239
240
const info = describe_quota(quota, true);
241
return <span>{info}</span>;
242
}
243
244
export function DateRange({ activates, expires, info }) {
245
const isExpired = expires && expires < Date.now();
246
const sub = info?.purchased?.subscription;
247
if (sub && sub != "no") {
248
return (
249
<span>
250
{capitalize(sub)} subscription
251
{isExpired && (
252
<>
253
<b> Expired </b> <Timestamp epoch={expires} absolute dateOnly />
254
</>
255
)}
256
</span>
257
);
258
}
259
const dates = (
260
<>
261
<Timestamp epoch={activates} absolute dateOnly /> &ndash;{" "}
262
<Timestamp epoch={expires} absolute dateOnly />
263
</>
264
);
265
return (
266
<span>
267
{isExpired ? (
268
<>
269
<b>Expired </b>(was valid {dates})
270
</>
271
) : (
272
<>Valid: {dates}</>
273
)}
274
</span>
275
);
276
}
277
278