Path: blob/master/src/packages/frontend/admin/users/projects.tsx
1496 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Show a table with links to recently used projects (with most recent first) that78- account_id: have a given account_id as collaborator; here we9show only the most recently used projects by them,10not everything. This is sorted by when *they* used11it last.1213- license_id: has a given license applied: here we show all projects14that are currently running with this license actively15upgrading them. Projects are sorted by their16last_edited field.1718*/1920import { Component, Rendered } from "@cocalc/frontend/app-framework";21import { cmp, keys, trunc_middle } from "@cocalc/util/misc";22import { Loading, TimeAgo } from "@cocalc/frontend/components";23import { query } from "@cocalc/frontend/frame-editors/generic/client";24import { Card } from "antd";25import { Row, Col } from "@cocalc/frontend/antd-bootstrap";26import { Button } from "antd";2728interface Project {29project_id: string;30title: string;31description: string;32users: Map<string, any>;33last_active: Map<string, any>;34last_edited: Date;35}3637interface Props {38account_id?: string; // one of account_id or license_id must be given; see comments above39license_id?: string;40cutoff?: "now" | Date; // if given, and showing projects for a license, show projects that ran back to cutoff.41title?: string | Rendered; // Defaults to "Projects"42}4344interface State {45status?: string;46number?: number; // number of projects -- only used for license_id47projects?: Project[]; // actual information about the projects48load_projects?: boolean;49}5051function project_sort_key(52project: Project,53account_id?: string,54): string | Date {55if (!account_id) return project.last_edited ?? new Date(0);56if (project.last_active && project.last_active[account_id]) {57return project.last_active[account_id];58}59return "";60}6162export class Projects extends Component<Props, State> {63private mounted: boolean = false;6465constructor(props, state) {66super(props, state);67this.state = {};68}6970UNSAFE_componentWillMount(): void {71this.mounted = true;72this.update_search();73}7475componentWillUnmount(): void {76this.mounted = false;77}7879componentDidUpdate(prevProps) {80if (this.props.cutoff != prevProps.cutoff) {81this.setState({ load_projects: false });82this.update_search();83}84}8586status_mesg(s: string): void {87this.setState({88status: s,89});90}9192private get_cutoff(): undefined | Date {93return !this.props.cutoff || this.props.cutoff == "now"94? undefined95: this.props.cutoff;96}9798private query() {99if (this.props.account_id) {100return {101query: {102projects: [103{104project_id: null,105title: null,106description: null,107users: null,108last_active: null,109last_edited: null,110},111],112},113options: [{ account_id: this.props.account_id }],114};115} else if (this.props.license_id) {116const cutoff = this.get_cutoff();117return {118query: {119projects_using_site_license: [120{121license_id: this.props.license_id,122project_id: null,123title: null,124description: null,125users: null,126last_active: null,127last_edited: null,128cutoff,129},130],131},132};133} else {134throw Error("account_id or license_id must be specified");135}136}137138async update_search(): Promise<void> {139try {140if (this.props.account_id || this.state.load_projects) {141await this.load_projects();142} else {143await this.load_number();144}145} catch (err) {146this.status_mesg(`ERROR -- ${err}`);147}148}149150// Load the projects151async load_projects(): Promise<void> {152this.status_mesg("Loading projects...");153const q = this.query();154const table = keys(q.query)[0];155const projects: Project[] = (await query(q)).query[table];156if (!this.mounted) {157return;158}159projects.sort(160(a, b) =>161-cmp(162project_sort_key(a, this.props.account_id),163project_sort_key(b, this.props.account_id),164),165);166this.status_mesg("");167this.setState({ projects: projects, number: projects.length });168}169170// Load the number of projects171async load_number(): Promise<void> {172this.status_mesg("Counting projects...");173const cutoff = this.get_cutoff();174const q = {175query: {176number_of_projects_using_site_license: {177license_id: this.props.license_id,178number: null,179cutoff,180},181},182};183const { number } = (await query(q)).query184.number_of_projects_using_site_license;185if (!this.mounted) {186return;187}188this.status_mesg("");189this.setState({ number });190}191192private render_load_projects_button(): Rendered {193if (this.props.account_id || this.state.load_projects) return;194if (this.state.number != null && this.state.number == 0) {195return <div>No projects</div>;196}197198return (199<Button onClick={() => this.click_load_projects_button()}>200Show {this.state.number != null ? `${this.state.number} ` : ""}project201{this.state.number != 1 ? "s" : ""}...202</Button>203);204}205206private click_load_projects_button(): void {207this.setState({ load_projects: true });208this.load_projects();209}210211render_number_of_projects(): Rendered {212if (this.state.number == null) {213return;214}215return <span>({this.state.number})</span>;216}217218render_projects(): Rendered {219if (this.props.license_id != null && !this.state.load_projects) {220return this.render_load_projects_button();221}222223if (!this.state.projects) {224return <Loading />;225}226227if (this.state.projects.length == 0) {228return <div>No projects</div>;229}230231const v: Rendered[] = [this.render_header()];232233let project: Project;234let i = 0;235for (project of this.state.projects) {236const style = i % 2 ? { backgroundColor: "#f8f8f8" } : undefined;237i += 1;238239v.push(this.render_project(project, style));240}241return <div>{v}</div>;242}243244render_last_active(project: Project): Rendered {245if (!this.props.account_id) {246return <TimeAgo date={project.last_edited} />;247}248if (project.last_active && project.last_active[this.props.account_id]) {249return <TimeAgo date={project.last_active[this.props.account_id]} />;250}251return <span />;252}253254render_description(project: Project): Rendered {255if (project.description == "No Description") {256return;257}258return <span>{trunc_middle(project.description, 60)}</span>;259}260261render_project(project: Project, style?: React.CSSProperties): Rendered {262return (263<Row key={project.project_id} style={style}>264<Col md={4}>{trunc_middle(project.title, 60)}</Col>265<Col md={4}>{this.render_description(project)}</Col>266<Col md={4}>{this.render_last_active(project)}</Col>267</Row>268);269}270271render_header(): Rendered {272return (273<Row key="header" style={{ fontWeight: "bold", color: "#666" }}>274<Col md={4}>Title</Col>275<Col md={4}>Description</Col>276<Col md={4}>Active</Col>277</Row>278);279}280281render(): Rendered {282const content = this.state.status ? (283this.state.status284) : (285<span>286{this.props.title} {this.render_number_of_projects()}287</span>288);289const title = (290<div style={{ fontWeight: "bold", color: "#666", width: "100%" }}>291{content}292</div>293);294return <Card title={title}>{this.render_projects()}</Card>;295}296}297298299