Skip to content

Commit 9f62a5c

Browse files
committed
Merge branch 'master' into ws
2 parents 465ac59 + 5bc16ba commit 9f62a5c

File tree

6 files changed

+221
-49
lines changed

6 files changed

+221
-49
lines changed

src/packages/frontend/customize.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface CustomizeState {
126126
logo_square: string;
127127
max_upgrades: TypedMap<Partial<Upgrades>>;
128128
nonfree_countries?: List<string>;
129+
limit_free_project_uptime: number; // minutes
129130
onprem_quota_heading: string;
130131
organization_email: string;
131132
organization_name: string;

src/packages/frontend/editors/stopwatch/stopwatch.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import {
1414
PlayCircleTwoTone,
1515
StopTwoTone,
1616
} from "@ant-design/icons";
17-
import { redux, useForceUpdate } from "@cocalc/frontend/app-framework";
18-
import { Icon } from "@cocalc/frontend/components/icon";
1917
import { Button, Col, Modal, Row, TimePicker, Tooltip } from "antd";
2018
import type { Dayjs } from "dayjs";
2119
import dayjs from "dayjs";
2220
import { CSSProperties, useEffect, useState } from "react";
2321

22+
import { redux, useForceUpdate } from "@cocalc/frontend/app-framework";
23+
import { Icon } from "@cocalc/frontend/components/icon";
2424
import MarkdownInput from "@cocalc/frontend/editors/markdown-input/multimode";
2525
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
2626
import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";
@@ -36,7 +36,7 @@ interface StopwatchProps {
3636
state: TimerState; // 'paused' or 'running' or 'stopped'
3737
time: number; // when entered this state
3838
countdown?: number; // if given, this is a countdown timer, counting down from this many seconds.
39-
clickButton: (str: string) => void;
39+
clickButton?: (str: string) => void;
4040
setLabel?: (str: string) => void;
4141
setCountdown?: (time: number) => void; // time in seconds
4242
compact?: boolean;
@@ -71,7 +71,7 @@ export default function Stopwatch(props: StopwatchProps) {
7171
>
7272
<Button
7373
icon={<PlayCircleTwoTone />}
74-
onClick={() => props.clickButton("start")}
74+
onClick={() => props.clickButton?.("start")}
7575
style={!props.compact ? { width: "8em" } : undefined}
7676
>
7777
{!props.compact ? "Start" : undefined}
@@ -98,7 +98,7 @@ export default function Stopwatch(props: StopwatchProps) {
9898
>
9999
<Button
100100
icon={<StopTwoTone />}
101-
onClick={() => props.clickButton("reset")}
101+
onClick={() => props.clickButton?.("reset")}
102102
>
103103
{!props.compact ? "Reset" : undefined}
104104
</Button>
@@ -126,7 +126,7 @@ export default function Stopwatch(props: StopwatchProps) {
126126
time.second() + time.minute() * 60 + time.hour() * 60 * 60,
127127
);
128128
// timeout so the setcountdown can fully propagate through flux; needed for whiteboard
129-
setTimeout(() => props.clickButton("reset"), 0);
129+
setTimeout(() => props.clickButton?.("reset"), 0);
130130
}
131131
}}
132132
showNow={false}
@@ -152,7 +152,7 @@ export default function Stopwatch(props: StopwatchProps) {
152152
>
153153
<Button
154154
icon={<DeleteTwoTone />}
155-
onClick={() => props.clickButton("delete")}
155+
onClick={() => props.clickButton?.("delete")}
156156
>
157157
{!props.compact ? "Delete" : undefined}
158158
</Button>
@@ -165,7 +165,7 @@ export default function Stopwatch(props: StopwatchProps) {
165165
<Tooltip mouseEnterDelay={1} title="Pause the stopwatch">
166166
<Button
167167
icon={<PauseCircleTwoTone />}
168-
onClick={() => props.clickButton("pause")}
168+
onClick={() => props.clickButton?.("pause")}
169169
style={!props.compact ? { width: "8em" } : undefined}
170170
>
171171
{!props.compact ? "Pause" : undefined}
@@ -229,13 +229,13 @@ export default function Stopwatch(props: StopwatchProps) {
229229
}
230230
open
231231
onOk={() => {
232-
props.clickButton("reset");
232+
props.clickButton?.("reset");
233233
redux
234234
.getProjectActions(frame.project_id)
235235
?.open_file({ path: frame.path });
236236
}}
237237
onCancel={() => {
238-
props.clickButton("reset");
238+
props.clickButton?.("reset");
239239
}}
240240
>
241241
{props.label && <StaticMarkdown value={props.label} />}

src/packages/frontend/project/trial-banner.tsx

Lines changed: 188 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,41 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6-
import { Alert, Tag } from "antd";
6+
import { Alert, Modal, Space, Tag } from "antd";
77
import humanizeList from "humanize-list";
88
import { join } from "path";
99

1010
import {
1111
CSS,
1212
React,
13+
redux,
14+
useEffect,
15+
useForceUpdate,
1316
useMemo,
17+
useRef,
1418
useState,
15-
redux,
19+
useTypedRedux,
1620
} from "@cocalc/frontend/app-framework";
17-
import { A, Icon, Paragraph } from "@cocalc/frontend/components";
21+
import { A, Icon, Paragraph, Text } from "@cocalc/frontend/components";
22+
import { SiteName } from "@cocalc/frontend/customize";
1823
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
24+
import { TimeAmount } from "@cocalc/frontend/editors/stopwatch/time";
25+
import { open_new_tab } from "@cocalc/frontend/misc";
1926
import {
2027
SiteLicenseInput,
2128
useManagedLicenses,
2229
} from "@cocalc/frontend/site-licenses/input";
2330
import { BuyLicenseForProject } from "@cocalc/frontend/site-licenses/purchase/buy-license-for-project";
31+
import track from "@cocalc/frontend/user-tracking";
2432
import {
2533
BANNER_NON_DISMISSABLE_DAYS,
2634
EVALUATION_PERIOD_DAYS,
2735
LICENSE_MIN_PRICE,
2836
} from "@cocalc/util/consts/billing";
29-
import { server_time } from "@cocalc/util/relative-time";
37+
import { server_time } from "@cocalc/util/misc";
3038
import { COLORS, DOC_URL } from "@cocalc/util/theme";
3139
import { useAllowedFreeProjectToRun } from "./client-side-throttle";
40+
import { useProjectContext } from "./context";
3241
import { applyLicense } from "./settings/site-license";
3342

3443
export const DOC_TRIAL = "https://doc.cocalc.com/trial.html";
@@ -133,22 +142,22 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
133142
<strong>No upgrades</strong>
134143
);
135144

136-
function renderComputeServer() {
137-
return (
138-
<a
139-
style={a_style}
140-
onClick={() => {
141-
const actions = redux.getProjectActions(project_id);
142-
actions.setState({ create_compute_server: true });
143-
actions.set_active_tab("servers", {
144-
change_history: true,
145-
});
146-
}}
147-
>
148-
using a compute server
149-
</a>
150-
);
151-
}
145+
// function renderComputeServer() {
146+
// return (
147+
// <a
148+
// style={a_style}
149+
// onClick={() => {
150+
// const actions = redux.getProjectActions(project_id);
151+
// actions.setState({ create_compute_server: true });
152+
// actions.set_active_tab("servers", {
153+
// change_history: true,
154+
// });
155+
// }}
156+
// >
157+
// using a compute server
158+
// </a>
159+
// );
160+
// }
152161

153162
function renderBuyAndUpgrade(text: string = "with a license"): JSX.Element {
154163
return (
@@ -160,9 +169,10 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
160169
asLink={true}
161170
style={{ padding: 0, fontSize: style.fontSize, ...a_style }}
162171
/>
163-
. Price starts at {LICENSE_MIN_PRICE}.{" "}
172+
.<br />
173+
Price starts at {LICENSE_MIN_PRICE}.{" "}
164174
<a style={a_style} onClick={() => setShowAddLicense(true)}>
165-
Apply your license to this project.
175+
Apply your license to this project
166176
</a>
167177
</>
168178
);
@@ -183,7 +193,7 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
183193
return (
184194
<span>
185195
{trial_project} You can improve hosting quality and get internet
186-
access {renderComputeServer()} or {renderBuyAndUpgrade()}.
196+
access {/* {renderComputeServer()} */} or {renderBuyAndUpgrade()}.
187197
<br />
188198
Otherwise, {humanizeList([...NO_HOST, NO_INTERNET])}
189199
{"."}
@@ -251,20 +261,28 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
251261
return null;
252262
}
253263

264+
function renderClose() {
265+
return (
266+
<Tag
267+
style={{ marginTop: "10px", fontSize: style.fontSize }}
268+
color="#faad14"
269+
>
270+
<Icon name="times" /> Dismiss
271+
</Tag>
272+
);
273+
}
274+
275+
function renderCountDown() {
276+
if (closable) return;
277+
278+
return <CountdownProject fontSize={style.fontSize} />;
279+
}
280+
254281
return (
255282
<Alert
256283
type="warning"
257284
closable={closable}
258-
closeIcon={
259-
closable ? (
260-
<Tag
261-
style={{ marginTop: "10px", fontSize: style.fontSize }}
262-
color="#faad14"
263-
>
264-
<Icon name="times" /> Dismiss
265-
</Tag>
266-
) : undefined
267-
}
285+
closeIcon={renderClose()}
268286
style={style}
269287
banner={true}
270288
showIcon={!closable || (internet && host)}
@@ -286,15 +304,16 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
286304
padding: 0,
287305
}}
288306
>
307+
{renderCountDown()}
289308
{renderMessage()} {renderLearnMore(style.color)}
290309
</Paragraph>
291-
{showAddLicense && (
310+
{showAddLicense ? (
292311
<BannerApplySiteLicense
293312
project_id={project_id}
294313
projectSiteLicenses={projectSiteLicenses}
295314
setShowAddLicense={setShowAddLicense}
296315
/>
297-
)}
316+
) : undefined}
298317
</>
299318
}
300319
/>
@@ -356,3 +375,137 @@ export const BannerApplySiteLicense: React.FC<ApplyLicenseProps> = (
356375
</>
357376
);
358377
};
378+
379+
interface CountdownProjectProps {
380+
fontSize: CSS["fontSize"];
381+
}
382+
383+
function CountdownProject({ fontSize }: CountdownProjectProps) {
384+
const { status, project, project_id, actions } = useProjectContext();
385+
const limit_min = useTypedRedux("customize", "limit_free_project_uptime");
386+
const [showInfo, setShowInfo] = useState<boolean>(false);
387+
const openFiles = useTypedRedux({ project_id }, "open_files_order");
388+
const triggered = useRef<boolean>(false);
389+
const update = useForceUpdate();
390+
391+
useEffect(() => {
392+
const interval = setInterval(update, 1000);
393+
return () => clearInterval(interval);
394+
}, []);
395+
396+
if (
397+
status.get("state") !== "running" ||
398+
project == null ||
399+
limit_min == null ||
400+
limit_min <= 0
401+
) {
402+
return null;
403+
}
404+
405+
// start_ts is e.g. 1508576664416
406+
const start_ts = project.getIn(["status", "start_ts"]);
407+
if (start_ts == undefined) return null;
408+
409+
const shutdown_ts = start_ts + 1000 * 60 * limit_min;
410+
const countdown = shutdown_ts - server_time().getTime();
411+
const countdwon0 = countdown > 0 ? countdown : 0;
412+
413+
if (countdown < 0 && !triggered.current) {
414+
triggered.current = true;
415+
416+
// This closes all tabs and then stops the project.
417+
openFiles.map((path) => actions?.close_tab(path));
418+
redux.getActions("projects").stop_project(project_id);
419+
}
420+
421+
function renderInfo() {
422+
return (
423+
<Modal
424+
title={
425+
<Space>
426+
<Icon name="hand-stop" /> Automatic Project Shutdown
427+
</Space>
428+
}
429+
open={showInfo}
430+
onOk={() => open_new_tab(BUY_A_LICENSE_URL)}
431+
onCancel={() => setShowInfo(false)}
432+
>
433+
<Paragraph>
434+
<A href={"https://doc.cocalc.com/trial.html"}>Trial projects</A> have
435+
a maximum uptime of {limit_min} minutes. After that period, the
436+
project will stop and interrupt your work.
437+
</Paragraph>
438+
<Paragraph strong>
439+
This shutdown timer only exists for projects without any upgrades!
440+
</Paragraph>
441+
<Alert
442+
banner
443+
type="info"
444+
showIcon={false}
445+
message={
446+
<>
447+
<Paragraph strong>
448+
This is a call to support <SiteName /> by{" "}
449+
<A href={BUY_A_LICENSE_URL}>purchasing a license</A>.
450+
</Paragraph>
451+
<Paragraph>
452+
Behind this curtains,{" "}
453+
<A href={"/about/team"}>humans are working hard</A> to keep the
454+
service running and improving it constantly. Your files and
455+
computations <A href={"/info/status"}>run in our cluster</A>,
456+
which costs money as well.
457+
</Paragraph>
458+
<Paragraph>
459+
<SiteName /> receives no funding from large venture captital
460+
organizations or charitable foundations. The site depends
461+
entirely <Text strong>on your financial support</Text> to
462+
continue operating. Without your financial support this service
463+
will not survive long-term!
464+
</Paragraph>
465+
<Paragraph>
466+
<A
467+
href={
468+
"/support/new?hideExtra=true&type=purchase&subject=Support+CoCalc&title=Support+CoCalc"
469+
}
470+
>
471+
Contact us
472+
</A>{" "}
473+
if you can give support in other ways.
474+
</Paragraph>
475+
</>
476+
}
477+
/>
478+
</Modal>
479+
);
480+
}
481+
482+
return (
483+
<>
484+
{renderInfo()}
485+
<Tag
486+
style={{
487+
marginTop: "5px",
488+
fontSize,
489+
float: "right",
490+
fontWeight: "bold",
491+
color: COLORS.ANTD_RED,
492+
cursor: "pointer",
493+
}}
494+
color={COLORS.GRAY_LL}
495+
onClick={() => {
496+
setShowInfo(true);
497+
track("trial-banner", { what: "countdown", project_id });
498+
}}
499+
>
500+
<TimeAmount
501+
key={"time"}
502+
amount={countdwon0}
503+
compact={true}
504+
showIcon={true}
505+
countdown={countdwon0}
506+
style={{ color: COLORS.ANTD_RED }}
507+
/>
508+
</Tag>
509+
</>
510+
);
511+
}

0 commit comments

Comments
 (0)