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/next/components/licenses/managed.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Button,8Checkbox,9Input,10Popconfirm,11Popover,12Table,13} from "antd";14import { useMemo, useState } from "react";1516import { Icon } from "@cocalc/frontend/components/icon";17import Markdown from "@cocalc/frontend/editors/slate/static-markdown";18import { cmp, plural, search_match, search_split } from "@cocalc/util/misc";19import Avatar from "components/account/avatar";20import UserName from "components/account/name";21import SelectUsers from "components/account/select-users";22import { Paragraph, Title } from "components/misc";23import A from "components/misc/A";24import Timestamp from "components/misc/timestamp";25import Loading from "components/share/loading";26import apiPost from "lib/api/post";27import useAPI from "lib/hooks/api";28import useCustomize from "lib/use-customize";29import { EditableDescription, EditableTitle } from "./editable-license";30import License, { DateRange, Quota as LicenseQuota } from "./license";3132const renderTimestamp = (epoch) => <Timestamp epoch={epoch} />;3334export const quotaColumn = {35title: (36<Popover37title="Quota"38content={39<div style={{ maxWidth: "75ex" }}>40This is the license quota. If the license is active on a project, its41quotas will be set to at least the values listed here.42</div>43}44>45Quota{" "}46</Popover>47),48width: "35%",49responsive: ["sm"],50render: (_, license) => <Quota {...license} />,51};5253export function Quota({ quota, state, upgrades }) {54return state != null && state != "running" ? (55<span>—</span>56) : (57<span58style={{59wordWrap: "break-word",60wordBreak: "break-word",61}}62>63{quota && <LicenseQuota quota={quota} />}64{/* upgrades is deprecated, but in case we encounter it, do not ignore it */}65{upgrades && (66<Markdown67value={"```js\n" + JSON.stringify(upgrades, undefined, 2) + "\n```"}68/>69)}70</span>71);72}7374function TitleDescId({ title, description, id, onChange }) {75return (76<div77style={{78wordWrap: "break-word",79wordBreak: "break-word",80color: "#333",81}}82>83<div style={{ fontFamily: "monospace", fontSize: "9pt" }}>84<License license_id={id} />85</div>86<EditableTitle license_id={id} title={title} onChange={onChange} />87<EditableDescription88license_id={id}89description={description}90onChange={onChange}91/>92</div>93);94}9596function Managers({ managers, id, onChange }) {97return (98<>99<div style={{ maxHeight: "65px", overflowY: "auto" }}>100{managers.map((account_id) => (101<Avatar102style={{ margin: "0 5px 5px 0" }}103key={account_id}104account_id={account_id}105size={24}106extra={107<RemoveManager108license_id={id}109managers={managers}110account_id={account_id}111onChange={onChange}112/>113}114/>115))}116</div>117<AddManagers license_id={id} managers={managers} onChange={onChange} />118</>119);120}121122function RunLimit({ run_limit }) {123return <>{run_limit}</>;124}125126function LastUsed({ last_used }) {127return renderTimestamp(last_used);128}129130function Created({ created }) {131return renderTimestamp(created);132}133134function columns(onChange) {135return [136{137responsive: ["xs"],138title: "Managed Licenses",139render: (_, license) => (140<div>141<TitleDescId {...license} onChange={onChange} />142<div>143<DateRange {...license} />144</div>{" "}145Run Limit: <RunLimit {...license} />146<div>147Quota: <Quota {...license} />148</div>149Last Used: <LastUsed {...license} />150<br />151Created: <Created {...license} />152<div style={{ border: "1px solid lightgrey", padding: "5px 15px" }}>153Managers <Managers {...license} onChange={onChange} />154</div>155</div>156),157},158{159responsive: ["sm"],160title: (161<Popover162placement="top"163title="Id, Title and Description of the License"164content={165<div style={{ maxWidth: "75ex" }}>166The first line is the id of the license, which anybody can enter167in various places to upgrade projects or courses. The title and168description of the license help you keep track of what the license169is for, and you can edit both fields here as well by clicking on170them.171</div>172}173>174License175</Popover>176),177key: "title",178width: "40%",179sorter: { compare: (a, b) => cmp(a.title, b.title) },180render: (_, license) => (181<div>182<TitleDescId {...license} onChange={onChange} />183<div>184<DateRange {...license} />185</div>186</div>187),188},189{190responsive: ["sm"],191width: "15%",192title: (193<Popover194title="Managers"195content={196<div style={{ maxWidth: "75ex" }}>197These are the managers of this license. They can see extra198information about the license, the license is included in any199dropdown where they can select a license, and they can add or200remove other license managers. You are a manager of all licenses201listed here.202</div>203}204>205Managers206</Popover>207),208key: "managers",209render: (_, license) => <Managers {...license} onChange={onChange} />,210},211{212responsive: ["sm"],213title: (214<Popover215placement="top"216title="Run Limit"217content={218<div style={{ maxWidth: "75ex" }}>219The maximum number of simultaneous running projects that this220license can upgrade. You can apply the license to any number of221projects, but it only impacts this many projects at once.222</div>223}224>225Run Limit226</Popover>227),228align: "center",229render: (_, license) => <RunLimit {...license} />,230sorter: { compare: (a, b) => cmp(a.run_limit, b.run_limit) },231},232quotaColumn,233{234responsive: ["sm"],235title: (236<Popover237placement="top"238title="When License was Last Used"239content={240<div style={{ maxWidth: "75ex" }}>241This is when this license was last used to upgrade a project when242the project was starting. It's the point in time when the project243started.244</div>245}246>247Last Used{" "}248</Popover>249),250render: (_, license) => <LastUsed {...license} />,251sorter: { compare: (a, b) => cmp(a.last_used, b.last_used) },252},253{254responsive: ["sm"],255title: (256<Popover257placement="top"258title="When License was Created"259content={260<div style={{ maxWidth: "75ex" }}>261This is when the license was created.262</div>263}264>265Created{" "}266</Popover>267),268render: (_, license) => <Created {...license} />,269sorter: { compare: (a, b) => cmp(a.created, b.created) },270},271];272}273274export default function ManagedLicenses() {275let { result, error, call } = useAPI("licenses/get-managed");276const [search, setSearch] = useState<string>("");277const [showExpired, setShowExpired] = useState<boolean>(false);278const numExpired: number = useMemo(() => {279if (!result) return 0;280let n = 0;281const t = Date.now();282for (const x of result) {283if (x.expires && x.expires <= t) {284n += 1;285}286}287return n;288}, [result]);289290if (error) {291return <Alert type="error" message={error} />;292}293if (!result) {294return <Loading style={{ fontSize: "16pt", margin: "auto" }} />;295}296297if (search) {298result = doSearch(result, search);299}300if (!showExpired) {301// filter out anything that is expired302result = removeExpired(result);303}304305function onChange() {306call();307}308309return (310<div style={{ width: "100%", overflowX: "auto", minHeight: "50vh" }}>311<Title level={2}>Licenses that you Manage ({result.length})</Title>312<Paragraph>313These are the licenses that you have purchased or been added to manage.314You can add other people as managers of any of these licenses, if they315need to be able to use these licenses to upgrade projects. You can also{" "}316<A href="/billing/subscriptions">manage your purchased subscriptions</A>{" "}317and browse <A href="/billing/receipts">your receipts and invoices</A>.318</Paragraph>319<Paragraph>320You can also{" "}321<A href="/settings/licenses" external>322edit or cancel for a refund any license that you purchased...323</A>324</Paragraph>325<Paragraph style={{ margin: "15px 0" }}>326<Checkbox327disabled={numExpired == 0}328style={{ float: "right" }}329checked={showExpired}330onChange={(e) => setShowExpired(e.target.checked)}331>332Show Expired ({numExpired})333</Checkbox>334<Input.Search335placeholder="Search..."336allowClear337onChange={(e) => setSearch(e.target.value)}338style={{ width: "50ex", maxWidth: "100%" }}339/>340</Paragraph>341<Table342columns={columns(onChange) as any}343dataSource={result}344rowKey={"id"}345style={{ marginTop: "15px" }}346pagination={{ hideOnSinglePage: true, pageSize: 100 }}347/>348</div>349);350}351352function doSearch(data: object[], search: string): object[] {353const v = search_split(search.toLowerCase().trim());354const w: object[] = [];355for (const x of data) {356if (x["search"] == null) {357x["search"] = `${x["title"] ?? ""} ${x["description"] ?? ""} ${358x["id"]359} ${x["info"]?.purchased?.subscription}`.toLowerCase();360}361if (search_match(x["search"], v)) {362w.push(x);363}364}365return w;366}367368function removeExpired(data: { expires?: number }[]): { expires?: number }[] {369const data1: { expires?: number }[] = [];370const now = Date.now();371for (const x of data) {372if (!(x.expires != null && x.expires <= now)) {373data1.push(x);374}375}376return data1;377}378379interface AddManagersProps {380license_id: string;381managers: string[];382onChange?: () => void;383}384385function AddManagers({ license_id, managers, onChange }: AddManagersProps) {386const [adding, setAdding] = useState<boolean>(false);387const [accountIds, setAccountIds] = useState<string[]>([]);388const [error, setError] = useState<string>("");389const { account } = useCustomize();390return (391<div>392{adding && (393<Button394size="small"395style={{ float: "right" }}396onClick={() => {397setAdding(false);398setError("");399setAccountIds([]);400}}401>402Cancel403</Button>404)}405<Button406disabled={adding}407style={{ marginTop: "5px" }}408size="small"409onClick={() => {410setAdding(true);411setAccountIds([]);412setError("");413}}414>415<Icon name="plus-circle" /> Add416</Button>417{adding && (418<div style={{ width: "300px", marginTop: "5px" }}>419{error && <Alert type="error" message={error} />}420<Button421disabled={accountIds.length == 0}422onClick={async () => {423setError("");424const query = {425manager_site_licenses: {426id: license_id,427managers: managers.concat(accountIds),428},429};430try {431await apiPost("/user-query", { query });432setAdding(false);433onChange?.();434} catch (err) {435setError(err.message);436}437}}438style={{ marginBottom: "5px", width: "100%" }}439type="primary"440>441<Icon name="check" /> Add {accountIds.length}{" "}442{plural(accountIds.length, "selected user")}443</Button>444<SelectUsers445autoFocus446onChange={setAccountIds}447exclude={managers.concat(448account?.account_id ? [account.account_id] : [],449)}450/>451</div>452)}453</div>454);455}456457interface RemoveManagerProps {458license_id: string;459account_id: string;460managers: string[];461onChange?: () => void;462}463464function RemoveManager({465license_id,466managers,467account_id,468onChange,469}: RemoveManagerProps) {470const [error, setError] = useState<string>("");471const { account } = useCustomize();472return (473<Popconfirm474zIndex={20000 /* compare with user search */}475title={476<>477{account?.account_id == account_id ? (478<>479Remove <b>yourself</b> as a manager of this license?480</>481) : (482<>483Remove manager{" "}484<b>485<UserName account_id={account_id} />?486</b>487<br />488<UserName account_id={account_id} /> will no longer see this489license listed under licenses they manage.490</>491)}492<br /> The license will <i>not</i> be automatically removed from any493projects.494</>495}496onConfirm={async () => {497setError("");498const query = {499manager_site_licenses: {500id: license_id,501managers: managers.filter((x) => x != account_id),502},503};504try {505await apiPost("/user-query", { query });506onChange?.();507} catch (err) {508setError(err.message);509}510}}511okText={"Remove"}512cancelText={"Cancel"}513>514<div>515<a>Remove as Manager...</a>516{error && (517<Alert518type="error"519message={"Error Removing Manager"}520description={error}521/>522)}523</div>524</Popconfirm>525);526}527528529