From e7b58c107766fd72f2177057be5de3cf9713ef87 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 31 Oct 2023 13:39:51 +0800 Subject: [PATCH] user oboarding flow (#54) * init * telemetry toggle * finish styling and functionality * remove unused style * rever some styles * use goroutine in sending telementry updates * remove wave-modal class from AlertModal * icons and button state fixes * minor change with goroutines * use default cursor not not-allowed --- src/app/assets/icons/github.svg | 5 + src/app/assets/icons/help_filled.svg | 5 + src/app/assets/icons/line/modals.tsx | 367 ++++++++++++++++++++++++++ src/app/assets/icons/shield_check.svg | 8 + src/app/common/common.less | 92 +++++++ src/app/common/common.tsx | 27 ++ src/app/common/modals/modals.less | 103 ++++++++ src/app/common/modals/modals.tsx | 129 ++++++--- wavesrv/pkg/cmdrunner/cmdrunner.go | 25 +- 9 files changed, 720 insertions(+), 41 deletions(-) create mode 100644 src/app/assets/icons/github.svg create mode 100644 src/app/assets/icons/help_filled.svg create mode 100644 src/app/assets/icons/line/modals.tsx create mode 100644 src/app/assets/icons/shield_check.svg diff --git a/src/app/assets/icons/github.svg b/src/app/assets/icons/github.svg new file mode 100644 index 00000000..08d53d2e --- /dev/null +++ b/src/app/assets/icons/github.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/assets/icons/help_filled.svg b/src/app/assets/icons/help_filled.svg new file mode 100644 index 00000000..675a3f2a --- /dev/null +++ b/src/app/assets/icons/help_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/assets/icons/line/modals.tsx b/src/app/assets/icons/line/modals.tsx new file mode 100644 index 00000000..02bfc364 --- /dev/null +++ b/src/app/assets/icons/line/modals.tsx @@ -0,0 +1,367 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { boundMethod } from "autobind-decorator"; +import { If, For } from "tsx-control-statements/components"; +import cn from "classnames"; +import dayjs from "dayjs"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; +import { Markdown } from "../common"; +import * as util from "../../../util/util"; + +import { ReactComponent as XmarkIcon } from "../../assets/icons/line/xmark.svg"; +import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; +import { ReactComponent as ShieldCheck } from "../../assets/icons/line/shield_check.svg"; +import { ReactComponent as Help } from "../../assets/icons/line/help_filled.svg"; +import { ReactComponent as Github } from "../../assets/icons/line/github.svg"; + +dayjs.extend(localizedFormat); + +type OV = mobx.IObservableValue; + +@mobxReact.observer +class DisconnectedModal extends React.Component<{}, {}> { + logRef: any = React.createRef(); + showLog: mobx.IObservableValue = mobx.observable.box(false); + + @boundMethod + restartServer() { + GlobalModel.restartWaveSrv(); + } + + @boundMethod + tryReconnect() { + GlobalModel.ws.connectNow("manual"); + } + + componentDidMount() { + if (this.logRef.current != null) { + this.logRef.current.scrollTop = this.logRef.current.scrollHeight; + } + } + + componentDidUpdate() { + if (this.logRef.current != null) { + this.logRef.current.scrollTop = this.logRef.current.scrollHeight; + } + } + + @boundMethod + handleShowLog(): void { + mobx.action(() => { + this.showLog.set(!this.showLog.get()); + })(); + } + + render() { + let model = GlobalModel; + let logLine: string = null; + let idx: number = 0; + return ( +
+
+
+
+
Prompt Client Disconnected
+
+ +
+
+ +
+ {logLine} +
+
+
+
+
+
+
+ + Show Log + + + Hide Log + +
+
+ + +
+
+
+ ); + } +} + +@mobxReact.observer +class ClientStopModal extends React.Component<{}, {}> { + @boundMethod + refreshClient() { + GlobalModel.refreshClient(); + } + + render() { + let model = GlobalModel; + let cdata = model.clientData.get(); + let title = "Client Not Ready"; + return ( +
+
+
+
+
[prompt] {title}
+
+
+ +
Cannot get client data.
+
+
+
+ +
+
+
+ ); + } +} + +@mobxReact.observer +class LoadingSpinner extends React.Component<{}, {}> { + render() { + return ( +
+
+
+
+
+
+ ); + } +} + +@mobxReact.observer +class AlertModal extends React.Component<{}, {}> { + @boundMethod + closeModal(): void { + GlobalModel.cancelAlert(); + } + + @boundMethod + handleOK(): void { + GlobalModel.confirmAlert(); + } + + render() { + let message = GlobalModel.alertMessage.get(); + if (message == null) { + return null; + } + let title = message.title ?? (message.confirm ? "Confirm" : "Alert"); + let isConfirm = message.confirm; + return ( +
+
+
+
+

+ + {title} +

+
+ +
+
+ + + + +
+

{message.message}

+
+
+
+ +
+ Cancel +
+
+ OK +
+
+ +
+ OK +
+
+
+
+
+ ); + } +} + +@mobxReact.observer +class WelcomeModal extends React.Component<{}, {}> { + totalPages: number = 3; + pageNum: OV = mobx.observable.box(1, { name: "welcome-pagenum" }); + + @boundMethod + closeModal(): void { + mobx.action(() => { + GlobalModel.welcomeModalOpen.set(false); + })(); + } + + @boundMethod + goNext(): void { + mobx.action(() => { + this.pageNum.set(this.pageNum.get() + 1); + })(); + } + + @boundMethod + goPrev(): void { + mobx.action(() => { + this.pageNum.set(this.pageNum.get() - 1); + })(); + } + + renderDot(num: number): any { + if (num == this.pageNum.get()) { + return ; + } + return ; + } + + renderDots(): any { + let elems: any = []; + for (let i = 1; i <= this.totalPages; i++) { + let elem = this.renderDot(i); + elems.push(elem); + } + return elems; + } + + render() { + let pageNum = this.pageNum.get(); + return ( +
+
+
+
+
welcome to [prompt]
+
+ +
+
+
+

+ Prompt is a new terminal to help save you time and keep your command-line life organized. + Here's a couple quick tips to get your started! +

+
+
+ 1}> + + + +
+ +
+
{this.renderDots()}
+
+ + + + + + +
+
+
+ ); + } +} + +@mobxReact.observer +class TosModal extends React.Component<{}, {}> { + @boundMethod + acceptTos(): void { + GlobalCommandRunner.clientAcceptTos(); + } + + render() { + return ( +
+
+
+
+
+
Welcome to Wave Terminal!
+
Lets set everything for you
+
+
+
+ +
+
Telemetry
+
+
+
+ +
+
+
+ +
+
+ {/*

+ + Full Terms of Service + +

*/} +
+
+
+
+ Accept Terms of Service +
+
+
+
+
+ ); + } +} + +export { WelcomeModal, LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal }; diff --git a/src/app/assets/icons/shield_check.svg b/src/app/assets/icons/shield_check.svg new file mode 100644 index 00000000..62dcb641 --- /dev/null +++ b/src/app/assets/icons/shield_check.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app/common/common.less b/src/app/common/common.less index 6e6bf8b5..5685fe03 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -176,6 +176,77 @@ } } +.checkbox { + display: flex; + + input[type="checkbox"] { + height: 0; + width: 0; + } + + input[type="checkbox"] + label { + position: relative; + display: flex; + align-items: center; + color: #9e9e9e; + transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1); + } + input[type="checkbox"] + label > span { + display: flex; + justify-content: center; + align-items: center; + margin-right: 16px; + width: 20px; + height: 20px; + background: transparent; + border: 2px solid #9e9e9e; + border-radius: 2px; + cursor: pointer; + transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1); + } + + input[type="checkbox"] + label:hover, + input[type="checkbox"]:focus + label { + color: #fff; + } + input[type="checkbox"] + label:hover > span, + input[type="checkbox"]:focus + label > span { + background: rgba(255, 255, 255, 0.1); + } + input[type="checkbox"]:checked + label > ins { + height: 100%; + } + + input[type="checkbox"]:checked + label > span { + border: 10px solid @term-green; + } + input[type="checkbox"]:checked + label > span:before { + content: ""; + position: absolute; + top: -2px; + left: 3px; + width: 7px; + height: 12px; + border-right: 2px solid #fff; + border-bottom: 2px solid #fff; + transform: rotate(45deg); + transform-origin: 0% 100%; + animation: checkbox-check 500ms cubic-bezier(0.4, 0, 0.23, 1); + } + + @keyframes checkbox-check { + 0% { + opacity: 0; + } + 33% { + opacity: 0.5; + } + 100% { + opacity: 1; + } + } +} + .button.is-prompt-green { background-color: #222; color: @term-white; @@ -186,6 +257,27 @@ } } +.button.is-wave-green { + margin-top: 16px; + display: flex; + padding: 6px 16px !important; + color: @term-bright-white !important; + align-items: center; + gap: 4px; + border-radius: 6px !important; + font-size: 14px !important; + height: auto !important; + background: @term-green !important; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset; + + &:hover { + background-color: @term-green; + color: @term-bright-white !important; + box-shadow: none; + } +} + .button.is-plain, .button.is-prompt-cancel { background-color: #222; diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 6c77ff81..4953bcc6 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -99,6 +99,32 @@ class Toggle extends React.Component<{ checked: boolean; onChange: (value: boole } } +class Checkbox extends React.Component< + { checked: boolean; onChange: (value: boolean) => void; label: string; id: string }, + {} +> { + render() { + const { checked, onChange, label, id } = this.props; + + return ( +
+ onChange(e.target.checked)} + aria-checked={checked} + role="checkbox" + /> + +
+ ); + } +} + @mobxReact.observer class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> { render() { @@ -331,6 +357,7 @@ class SettingsError extends React.Component<{ errorMessage: OV }, {}> { export { CmdStrCode, Toggle, + Checkbox, renderCmdText, RemoteStatusLight, InlineSettingsTextEdit, diff --git a/src/app/common/modals/modals.less b/src/app/common/modals/modals.less index 90066311..5c687214 100644 --- a/src/app/common/modals/modals.less +++ b/src/app/common/modals/modals.less @@ -181,6 +181,109 @@ } } +.modal.wave-modal { + .modal-content { + display: flex; + padding: 32px 48px; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border-radius: 10px; + background: var(--olive-dark-1, #151715); + box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45), + 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; + + .modal-content-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + width: 100%; + + header { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sizing-sm, 12px); + width: 100%; + + .modal-title { + color: @term-bright-white; + text-align: center; + font-size: 20px; + font-style: normal; + font-weight: 300; + line-height: 20px; + } + + .modal-subtitle { + color: @term-white; + text-align: center; + + font-style: normal; + font-weight: 300; + line-height: 20px; + } + } + + .content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 32px; + width: 100%; + margin-bottom: 0; + + .item { + display: flex; + width: 100%; + align-items: center; + gap: 16px; + + .item-inner { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + flex: 1 0 0; + + .item-title { + color: @term-bright-white; + font-style: normal; + line-height: 20px; + } + + .item-text { + color: @term-white; + font-style: normal; + line-height: 20px; + } + + .item-field { + display: flex; + align-items: center; + gap: 8px; + } + } + } + } + } + + footer { + .button-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + button.disabled-button { + cursor: default; + } + } + } + } +} + .modal.welcome-modal { footer { .prev-button { diff --git a/src/app/common/modals/modals.tsx b/src/app/common/modals/modals.tsx index 9382197b..5d3427ed 100644 --- a/src/app/common/modals/modals.tsx +++ b/src/app/common/modals/modals.tsx @@ -12,9 +12,14 @@ import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; import { Markdown } from "../common"; import * as util from "../../../util/util"; +import { Toggle, Checkbox } from "../common"; +import { ClientDataType } from "../../../types/types"; import { ReactComponent as XmarkIcon } from "../../assets/icons/line/xmark.svg"; import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg"; +import shield from "../../assets/icons/shield_check.svg"; +import help from "../../assets/icons/help_filled.svg"; +import github from "../../assets/icons/github.svg"; dayjs.extend(localizedFormat); @@ -312,48 +317,112 @@ class WelcomeModal extends React.Component<{}, {}> { @mobxReact.observer class TosModal extends React.Component<{}, {}> { + state = { + isChecked: false, + }; + + @boundMethod + handleCheckboxChange(checked: boolean): void { + this.setState({ isChecked: checked }); + } + @boundMethod acceptTos(): void { GlobalCommandRunner.clientAcceptTos(); } + @boundMethod + handleChangeTelemetry(val: boolean): void { + if (val) { + GlobalCommandRunner.telemetryOn(false); + } else { + GlobalCommandRunner.telemetryOff(false); + } + } + render() { + let cdata: ClientDataType = GlobalModel.clientData.get(); + return ( -
+
-
-
Welcome to [prompt]
-
-
+
+
+
Welcome to Wave Terminal!
+
Lets set everything for you
+
-

Thank you for downloading Prompt!

-

- Prompt is a new terminal designed to help you save time and organize your command life. - Prompt is currently in beta. If you'd like to give feedback, run into problems, have - questions, or need help, please join the Prompt{" "} - - discord server - - . -

-

- Prompt is free to use, no email or registration required (unless you're using the cloud - features). -

-

- - Full Terms of Service - -

+
+ Privacy +
+
Telemetry
+
+ We don’t collect any personal info, only crash logs and IP address to make Wave + better. If you like, you can disable telemetry now or late. +
+
+ +
Basic Telemetry
+
+
+
+
+ Help +
+
Help
+
+ If you need any help or you have feature request, you can join{" "} + + our Discord + + . +
+
+
+
+ Github +
+
Like Wave? Give us a star
+
+ Rankings are very important for small startups like us, it helps other people to + know about us. If you like Wave, please consider giving us a star on our{" "} + + Github Repository + + . +
+
+
+
+
+ +
+
+ +
+
-
); diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 93dbbb1f..316ee40e 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -3677,12 +3677,13 @@ func setNoTelemetry(ctx context.Context, clientData *sstore.ClientData, noTeleme return fmt.Errorf("error trying to update client telemetry: %v", err) } log.Printf("client no-telemetry setting updated to %v\n", noTelemetryVal) - err = pcloud.SendNoTelemetryUpdate(ctx, clientOpts.NoTelemetry) - if err != nil { - // ignore error, just log - log.Printf("[error] sending no-telemetry update: %v\n", err) - log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") - } + go func() { + err := pcloud.SendNoTelemetryUpdate(ctx, clientOpts.NoTelemetry) + if err != nil { + log.Printf("[error] sending no-telemetry update: %v\n", err) + log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") + } + }() return nil } @@ -3698,11 +3699,13 @@ func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, err } - err = pcloud.SendTelemetry(ctx, false) - if err != nil { - // ignore error, but log - log.Printf("[error] sending telemetry update (in /telemetry:on): %v\n", err) - } + go func() { + err := pcloud.SendTelemetry(ctx, false) + if err != nil { + // ignore error, but log + log.Printf("[error] sending telemetry update (in /telemetry:on): %v\n", err) + } + }() clientData, err = sstore.EnsureClientData(ctx) if err != nil { return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)