Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/admin/users/projects.tsx
1496 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Show a table with links to recently used projects (with most recent first) that
8
9
- account_id: have a given account_id as collaborator; here we
10
show only the most recently used projects by them,
11
not everything. This is sorted by when *they* used
12
it last.
13
14
- license_id: has a given license applied: here we show all projects
15
that are currently running with this license actively
16
upgrading them. Projects are sorted by their
17
last_edited field.
18
19
*/
20
21
import { Component, Rendered } from "@cocalc/frontend/app-framework";
22
import { cmp, keys, trunc_middle } from "@cocalc/util/misc";
23
import { Loading, TimeAgo } from "@cocalc/frontend/components";
24
import { query } from "@cocalc/frontend/frame-editors/generic/client";
25
import { Card } from "antd";
26
import { Row, Col } from "@cocalc/frontend/antd-bootstrap";
27
import { Button } from "antd";
28
29
interface Project {
30
project_id: string;
31
title: string;
32
description: string;
33
users: Map<string, any>;
34
last_active: Map<string, any>;
35
last_edited: Date;
36
}
37
38
interface Props {
39
account_id?: string; // one of account_id or license_id must be given; see comments above
40
license_id?: string;
41
cutoff?: "now" | Date; // if given, and showing projects for a license, show projects that ran back to cutoff.
42
title?: string | Rendered; // Defaults to "Projects"
43
}
44
45
interface State {
46
status?: string;
47
number?: number; // number of projects -- only used for license_id
48
projects?: Project[]; // actual information about the projects
49
load_projects?: boolean;
50
}
51
52
function project_sort_key(
53
project: Project,
54
account_id?: string,
55
): string | Date {
56
if (!account_id) return project.last_edited ?? new Date(0);
57
if (project.last_active && project.last_active[account_id]) {
58
return project.last_active[account_id];
59
}
60
return "";
61
}
62
63
export class Projects extends Component<Props, State> {
64
private mounted: boolean = false;
65
66
constructor(props, state) {
67
super(props, state);
68
this.state = {};
69
}
70
71
UNSAFE_componentWillMount(): void {
72
this.mounted = true;
73
this.update_search();
74
}
75
76
componentWillUnmount(): void {
77
this.mounted = false;
78
}
79
80
componentDidUpdate(prevProps) {
81
if (this.props.cutoff != prevProps.cutoff) {
82
this.setState({ load_projects: false });
83
this.update_search();
84
}
85
}
86
87
status_mesg(s: string): void {
88
this.setState({
89
status: s,
90
});
91
}
92
93
private get_cutoff(): undefined | Date {
94
return !this.props.cutoff || this.props.cutoff == "now"
95
? undefined
96
: this.props.cutoff;
97
}
98
99
private query() {
100
if (this.props.account_id) {
101
return {
102
query: {
103
projects: [
104
{
105
project_id: null,
106
title: null,
107
description: null,
108
users: null,
109
last_active: null,
110
last_edited: null,
111
},
112
],
113
},
114
options: [{ account_id: this.props.account_id }],
115
};
116
} else if (this.props.license_id) {
117
const cutoff = this.get_cutoff();
118
return {
119
query: {
120
projects_using_site_license: [
121
{
122
license_id: this.props.license_id,
123
project_id: null,
124
title: null,
125
description: null,
126
users: null,
127
last_active: null,
128
last_edited: null,
129
cutoff,
130
},
131
],
132
},
133
};
134
} else {
135
throw Error("account_id or license_id must be specified");
136
}
137
}
138
139
async update_search(): Promise<void> {
140
try {
141
if (this.props.account_id || this.state.load_projects) {
142
await this.load_projects();
143
} else {
144
await this.load_number();
145
}
146
} catch (err) {
147
this.status_mesg(`ERROR -- ${err}`);
148
}
149
}
150
151
// Load the projects
152
async load_projects(): Promise<void> {
153
this.status_mesg("Loading projects...");
154
const q = this.query();
155
const table = keys(q.query)[0];
156
const projects: Project[] = (await query(q)).query[table];
157
if (!this.mounted) {
158
return;
159
}
160
projects.sort(
161
(a, b) =>
162
-cmp(
163
project_sort_key(a, this.props.account_id),
164
project_sort_key(b, this.props.account_id),
165
),
166
);
167
this.status_mesg("");
168
this.setState({ projects: projects, number: projects.length });
169
}
170
171
// Load the number of projects
172
async load_number(): Promise<void> {
173
this.status_mesg("Counting projects...");
174
const cutoff = this.get_cutoff();
175
const q = {
176
query: {
177
number_of_projects_using_site_license: {
178
license_id: this.props.license_id,
179
number: null,
180
cutoff,
181
},
182
},
183
};
184
const { number } = (await query(q)).query
185
.number_of_projects_using_site_license;
186
if (!this.mounted) {
187
return;
188
}
189
this.status_mesg("");
190
this.setState({ number });
191
}
192
193
private render_load_projects_button(): Rendered {
194
if (this.props.account_id || this.state.load_projects) return;
195
if (this.state.number != null && this.state.number == 0) {
196
return <div>No projects</div>;
197
}
198
199
return (
200
<Button onClick={() => this.click_load_projects_button()}>
201
Show {this.state.number != null ? `${this.state.number} ` : ""}project
202
{this.state.number != 1 ? "s" : ""}...
203
</Button>
204
);
205
}
206
207
private click_load_projects_button(): void {
208
this.setState({ load_projects: true });
209
this.load_projects();
210
}
211
212
render_number_of_projects(): Rendered {
213
if (this.state.number == null) {
214
return;
215
}
216
return <span>({this.state.number})</span>;
217
}
218
219
render_projects(): Rendered {
220
if (this.props.license_id != null && !this.state.load_projects) {
221
return this.render_load_projects_button();
222
}
223
224
if (!this.state.projects) {
225
return <Loading />;
226
}
227
228
if (this.state.projects.length == 0) {
229
return <div>No projects</div>;
230
}
231
232
const v: Rendered[] = [this.render_header()];
233
234
let project: Project;
235
let i = 0;
236
for (project of this.state.projects) {
237
const style = i % 2 ? { backgroundColor: "#f8f8f8" } : undefined;
238
i += 1;
239
240
v.push(this.render_project(project, style));
241
}
242
return <div>{v}</div>;
243
}
244
245
render_last_active(project: Project): Rendered {
246
if (!this.props.account_id) {
247
return <TimeAgo date={project.last_edited} />;
248
}
249
if (project.last_active && project.last_active[this.props.account_id]) {
250
return <TimeAgo date={project.last_active[this.props.account_id]} />;
251
}
252
return <span />;
253
}
254
255
render_description(project: Project): Rendered {
256
if (project.description == "No Description") {
257
return;
258
}
259
return <span>{trunc_middle(project.description, 60)}</span>;
260
}
261
262
render_project(project: Project, style?: React.CSSProperties): Rendered {
263
return (
264
<Row key={project.project_id} style={style}>
265
<Col md={4}>{trunc_middle(project.title, 60)}</Col>
266
<Col md={4}>{this.render_description(project)}</Col>
267
<Col md={4}>{this.render_last_active(project)}</Col>
268
</Row>
269
);
270
}
271
272
render_header(): Rendered {
273
return (
274
<Row key="header" style={{ fontWeight: "bold", color: "#666" }}>
275
<Col md={4}>Title</Col>
276
<Col md={4}>Description</Col>
277
<Col md={4}>Active</Col>
278
</Row>
279
);
280
}
281
282
render(): Rendered {
283
const content = this.state.status ? (
284
this.state.status
285
) : (
286
<span>
287
{this.props.title} {this.render_number_of_projects()}
288
</span>
289
);
290
const title = (
291
<div style={{ fontWeight: "bold", color: "#666", width: "100%" }}>
292
{content}
293
</div>
294
);
295
return <Card title={title}>{this.render_projects()}</Card>;
296
}
297
}
298
299