Skip to content

Commit 16f4b11

Browse files
committed
frontend/trial-banner: countdown timer triggering stop_project
1 parent df75554 commit 16f4b11

File tree

6 files changed

+204
-49
lines changed

6 files changed

+204
-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: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
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,
@@ -26,9 +33,10 @@ import {
2633
EVALUATION_PERIOD_DAYS,
2734
LICENSE_MIN_PRICE,
2835
} from "@cocalc/util/consts/billing";
29-
import { server_time } from "@cocalc/util/relative-time";
36+
import { server_time } from "@cocalc/util/misc";
3037
import { COLORS, DOC_URL } from "@cocalc/util/theme";
3138
import { useAllowedFreeProjectToRun } from "./client-side-throttle";
39+
import { useProjectContext } from "./context";
3240
import { applyLicense } from "./settings/site-license";
3341

3442
export const DOC_TRIAL = "https://doc.cocalc.com/trial.html";
@@ -133,22 +141,22 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
133141
<strong>No upgrades</strong>
134142
);
135143

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-
}
144+
// function renderComputeServer() {
145+
// return (
146+
// <a
147+
// style={a_style}
148+
// onClick={() => {
149+
// const actions = redux.getProjectActions(project_id);
150+
// actions.setState({ create_compute_server: true });
151+
// actions.set_active_tab("servers", {
152+
// change_history: true,
153+
// });
154+
// }}
155+
// >
156+
// using a compute server
157+
// </a>
158+
// );
159+
// }
152160

153161
function renderBuyAndUpgrade(text: string = "with a license"): JSX.Element {
154162
return (
@@ -160,9 +168,10 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
160168
asLink={true}
161169
style={{ padding: 0, fontSize: style.fontSize, ...a_style }}
162170
/>
163-
. Price starts at {LICENSE_MIN_PRICE}.{" "}
171+
.<br />
172+
Price starts at {LICENSE_MIN_PRICE}.{" "}
164173
<a style={a_style} onClick={() => setShowAddLicense(true)}>
165-
Apply your license to this project.
174+
Apply your license to this project
166175
</a>
167176
</>
168177
);
@@ -183,7 +192,7 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
183192
return (
184193
<span>
185194
{trial_project} You can improve hosting quality and get internet
186-
access {renderComputeServer()} or {renderBuyAndUpgrade()}.
195+
access {/* {renderComputeServer()} */} or {renderBuyAndUpgrade()}.
187196
<br />
188197
Otherwise, {humanizeList([...NO_HOST, NO_INTERNET])}
189198
{"."}
@@ -251,20 +260,28 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
251260
return null;
252261
}
253262

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

0 commit comments

Comments
 (0)