Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/admin/users/projects.tsx
Views: 687
/*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, redux } 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 { Row, Col, Panel } from "@cocalc/frontend/antd-bootstrap";25import { Button } from "antd";2627interface Project {28project_id: string;29title: string;30description: string;31users: Map<string, any>;32last_active: Map<string, any>;33last_edited: Date;34}3536interface Props {37account_id?: string; // one of account_id or license_id must be given; see comments above38license_id?: string;39cutoff?: "now" | Date; // if given, and showing projects for a license, show projects that ran back to cutoff.40title?: string | Rendered; // Defaults to "Projects"41}4243interface State {44status?: string;45number?: number; // number of projects -- only used for license_id46projects?: Project[]; // actual information about the projects47load_projects?: boolean;48}4950function project_sort_key(51project: Project,52account_id?: string53): string | Date {54if (!account_id) return project.last_edited ?? new Date(0);55if (project.last_active && project.last_active[account_id]) {56return project.last_active[account_id];57}58return "";59}6061export class Projects extends Component<Props, State> {62private mounted: boolean = false;6364constructor(props, state) {65super(props, state);66this.state = {};67}6869UNSAFE_componentWillMount(): void {70this.mounted = true;71this.update_search();72}7374componentWillUnmount(): void {75this.mounted = false;76}7778componentDidUpdate(prevProps) {79if (this.props.cutoff != prevProps.cutoff) {80this.setState({ load_projects: false });81this.update_search();82}83}8485status_mesg(s: string): void {86this.setState({87status: s,88});89}9091private get_cutoff(): undefined | Date {92return !this.props.cutoff || this.props.cutoff == "now"93? undefined94: this.props.cutoff;95}9697private query() {98if (this.props.account_id) {99return {100query: {101projects: [102{103project_id: null,104title: null,105description: null,106users: null,107last_active: null,108last_edited: null,109},110],111},112options: [{ account_id: this.props.account_id }],113};114} else if (this.props.license_id) {115const cutoff = this.get_cutoff();116return {117query: {118projects_using_site_license: [119{120license_id: this.props.license_id,121project_id: null,122title: null,123description: null,124users: null,125last_active: null,126last_edited: null,127cutoff,128},129],130},131};132} else {133throw Error("account_id or license_id must be specified");134}135}136137async update_search(): Promise<void> {138try {139if (this.props.account_id || this.state.load_projects) {140await this.load_projects();141} else {142await this.load_number();143}144} catch (err) {145this.status_mesg(`ERROR -- ${err}`);146}147}148149// Load the projects150async load_projects(): Promise<void> {151this.status_mesg("Loading projects...");152const q = this.query();153const table = keys(q.query)[0];154const projects: Project[] = (await query(q)).query[table];155if (!this.mounted) {156return;157}158projects.sort(159(a, b) =>160-cmp(161project_sort_key(a, this.props.account_id),162project_sort_key(b, this.props.account_id)163)164);165this.status_mesg("");166this.setState({ projects: projects, number: projects.length });167}168169// Load the number of projects170async load_number(): Promise<void> {171this.status_mesg("Counting projects...");172const cutoff = this.get_cutoff();173const q = {174query: {175number_of_projects_using_site_license: {176license_id: this.props.license_id,177number: null,178cutoff,179},180},181};182const { number } = (183await query(q)184).query.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}260261open_project(project_id: string): void {262const projects: any = redux.getActions("projects"); // todo: any?263projects.open_project({ project_id: project_id, switch_to: true });264}265266render_project(project: Project, style?: React.CSSProperties): Rendered {267return (268<Row key={project.project_id} style={style}>269<Col md={4}>270<a271style={{ cursor: "pointer" }}272onClick={() => this.open_project(project.project_id)}273>274{trunc_middle(project.title, 60)}275</a>276</Col>277<Col md={4}>{this.render_description(project)}</Col>278<Col md={4}>{this.render_last_active(project)}</Col>279</Row>280);281}282283render_header(): Rendered {284return (285<Row key="header" style={{ fontWeight: "bold", color: "#666" }}>286<Col md={4}>Title</Col>287<Col md={4}>Description</Col>288<Col md={4}>Active</Col>289</Row>290);291}292293render(): Rendered {294const content = this.state.status ? (295this.state.status296) : (297<span>298{this.props.title} {this.render_number_of_projects()}299</span>300);301const title = (302<span style={{ fontWeight: "bold", color: "#666" }}>{content}</span>303);304return <Panel header={title}>{this.render_projects()}</Panel>;305}306}307308309