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/frontend/admin/users/projects.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
/*
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, redux } 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 { Row, Col, Panel } from "@cocalc/frontend/antd-bootstrap";
26
import { Button } from "antd";
27
28
interface Project {
29
project_id: string;
30
title: string;
31
description: string;
32
users: Map<string, any>;
33
last_active: Map<string, any>;
34
last_edited: Date;
35
}
36
37
interface Props {
38
account_id?: string; // one of account_id or license_id must be given; see comments above
39
license_id?: string;
40
cutoff?: "now" | Date; // if given, and showing projects for a license, show projects that ran back to cutoff.
41
title?: string | Rendered; // Defaults to "Projects"
42
}
43
44
interface State {
45
status?: string;
46
number?: number; // number of projects -- only used for license_id
47
projects?: Project[]; // actual information about the projects
48
load_projects?: boolean;
49
}
50
51
function project_sort_key(
52
project: Project,
53
account_id?: string
54
): string | Date {
55
if (!account_id) return project.last_edited ?? new Date(0);
56
if (project.last_active && project.last_active[account_id]) {
57
return project.last_active[account_id];
58
}
59
return "";
60
}
61
62
export class Projects extends Component<Props, State> {
63
private mounted: boolean = false;
64
65
constructor(props, state) {
66
super(props, state);
67
this.state = {};
68
}
69
70
UNSAFE_componentWillMount(): void {
71
this.mounted = true;
72
this.update_search();
73
}
74
75
componentWillUnmount(): void {
76
this.mounted = false;
77
}
78
79
componentDidUpdate(prevProps) {
80
if (this.props.cutoff != prevProps.cutoff) {
81
this.setState({ load_projects: false });
82
this.update_search();
83
}
84
}
85
86
status_mesg(s: string): void {
87
this.setState({
88
status: s,
89
});
90
}
91
92
private get_cutoff(): undefined | Date {
93
return !this.props.cutoff || this.props.cutoff == "now"
94
? undefined
95
: this.props.cutoff;
96
}
97
98
private query() {
99
if (this.props.account_id) {
100
return {
101
query: {
102
projects: [
103
{
104
project_id: null,
105
title: null,
106
description: null,
107
users: null,
108
last_active: null,
109
last_edited: null,
110
},
111
],
112
},
113
options: [{ account_id: this.props.account_id }],
114
};
115
} else if (this.props.license_id) {
116
const cutoff = this.get_cutoff();
117
return {
118
query: {
119
projects_using_site_license: [
120
{
121
license_id: this.props.license_id,
122
project_id: null,
123
title: null,
124
description: null,
125
users: null,
126
last_active: null,
127
last_edited: null,
128
cutoff,
129
},
130
],
131
},
132
};
133
} else {
134
throw Error("account_id or license_id must be specified");
135
}
136
}
137
138
async update_search(): Promise<void> {
139
try {
140
if (this.props.account_id || this.state.load_projects) {
141
await this.load_projects();
142
} else {
143
await this.load_number();
144
}
145
} catch (err) {
146
this.status_mesg(`ERROR -- ${err}`);
147
}
148
}
149
150
// Load the projects
151
async load_projects(): Promise<void> {
152
this.status_mesg("Loading projects...");
153
const q = this.query();
154
const table = keys(q.query)[0];
155
const projects: Project[] = (await query(q)).query[table];
156
if (!this.mounted) {
157
return;
158
}
159
projects.sort(
160
(a, b) =>
161
-cmp(
162
project_sort_key(a, this.props.account_id),
163
project_sort_key(b, this.props.account_id)
164
)
165
);
166
this.status_mesg("");
167
this.setState({ projects: projects, number: projects.length });
168
}
169
170
// Load the number of projects
171
async load_number(): Promise<void> {
172
this.status_mesg("Counting projects...");
173
const cutoff = this.get_cutoff();
174
const q = {
175
query: {
176
number_of_projects_using_site_license: {
177
license_id: this.props.license_id,
178
number: null,
179
cutoff,
180
},
181
},
182
};
183
const { number } = (
184
await query(q)
185
).query.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
open_project(project_id: string): void {
263
const projects: any = redux.getActions("projects"); // todo: any?
264
projects.open_project({ project_id: project_id, switch_to: true });
265
}
266
267
render_project(project: Project, style?: React.CSSProperties): Rendered {
268
return (
269
<Row key={project.project_id} style={style}>
270
<Col md={4}>
271
<a
272
style={{ cursor: "pointer" }}
273
onClick={() => this.open_project(project.project_id)}
274
>
275
{trunc_middle(project.title, 60)}
276
</a>
277
</Col>
278
<Col md={4}>{this.render_description(project)}</Col>
279
<Col md={4}>{this.render_last_active(project)}</Col>
280
</Row>
281
);
282
}
283
284
render_header(): Rendered {
285
return (
286
<Row key="header" style={{ fontWeight: "bold", color: "#666" }}>
287
<Col md={4}>Title</Col>
288
<Col md={4}>Description</Col>
289
<Col md={4}>Active</Col>
290
</Row>
291
);
292
}
293
294
render(): Rendered {
295
const content = this.state.status ? (
296
this.state.status
297
) : (
298
<span>
299
{this.props.title} {this.render_number_of_projects()}
300
</span>
301
);
302
const title = (
303
<span style={{ fontWeight: "bold", color: "#666" }}>{content}</span>
304
);
305
return <Panel header={title}>{this.render_projects()}</Panel>;
306
}
307
}
308
309