This commit is contained in:
Sergey Garin 2024-04-25 12:02:19 +03:00
parent 627ee76e48
commit 04c66eca34
15 changed files with 199 additions and 60 deletions

View File

@ -308,7 +308,6 @@ export default [
],
'no-constant-condition': ['error', { checkLoops: false }],
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
'prefer-arrow-callback': 'error',
'prefer-const': 'error',
// Not relevant because TypeScript checks types.
'react/prop-types': 'off',

View File

@ -28,7 +28,8 @@
"opener": "^1.5.2",
"string-length": "^5.0.1",
"tar": "^6.1.13",
"yargs": "17.6.2"
"yargs": "17.6.2",
"mkcert": "3.2.0"
},
"comments": {
"electron-builder": "Cannot be updated to a newer version because of a NSIS installer issue: https://github.com/enso-org/enso/issues/5169"

View File

@ -6,6 +6,7 @@ import * as http from 'node:http'
import * as os from 'node:os'
import * as path from 'node:path'
import * as stream from 'node:stream'
import * as mkcert from 'mkcert'
import * as mime from 'mime-types'
import * as portfinder from 'portfinder'
@ -107,10 +108,28 @@ export class Server {
/** Start the server. */
run(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
const defaultValidity = 365
const ca = await mkcert.createCA({
organization: 'Hello CA',
countryCode: 'NP',
state: 'Bagmati',
locality: 'Kathmandu',
validity: defaultValidity,
})
const cert = await mkcert.createCert({
ca: { key: ca.key, cert: ca.cert },
domains: ['127.0.0.1', 'localhost'],
validity: defaultValidity,
})
createServer(
{
http: this.config.port,
https: {
key: cert.key,
cert: cert.cert,
port: this.config.port,
},
handler: this.process.bind(this),
},
(err, { http: httpServer }) => {

View File

@ -66,6 +66,18 @@ class App {
this.setProjectToOpenOnStartup(id)
})
electron.app.commandLine.appendSwitch('allow-insecure-localhost', 'true')
electron.app.commandLine.appendSwitch('ignore-certificate-errors', 'true')
electron.app.on(
'certificate-error',
(event, webContents, url, error, certificate, callback) => {
// Prevent having error
event.preventDefault()
// and continue
callback(true)
}
)
const { windowSize, chromeOptions, fileToOpen, urlToOpen } = this.processArguments()
this.handleItemOpening(fileToOpen, urlToOpen)
if (this.args.options.version.value) {
@ -439,7 +451,7 @@ class App {
searchParams[option.qualifiedName()] = option.value.toString()
}
}
const address = new URL('http://localhost')
const address = new URL('https://localhost')
address.port = this.serverPort().toString()
address.search = new URLSearchParams(searchParams).toString()
logger.log(`Loading the window address '${address.toString()}'.`)

View File

@ -3,6 +3,7 @@
import * as childProcess from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as mkcert from 'mkcert'
import * as esbuild from 'esbuild'

View File

@ -0,0 +1,15 @@
import * as React from 'react'
import * as aria from '#/components/aria'
/**
*
*/
export interface LinkProps extends aria.LinkProps {}
/**
*
*/
export function Link(props: LinkProps) {
return <aria.Link />
}

View File

@ -15,7 +15,28 @@ import SvgMask from '#/components/SvgMask'
// ==============
/** Props for a {@link Button}. */
export interface ButtonProps extends Readonly<aria.ButtonProps> {
export type ButtonProps =
| (aria.ButtonProps & BaseButtonProps & PropsWithoutHref)
| (aria.LinkProps & BaseButtonProps & PropsWithHref)
/**
* Props for a button with an href.
*/
interface PropsWithHref {
readonly href: string
}
/**
* Props for a button without an href.
*/
interface PropsWithoutHref {
readonly href?: never
}
/**
* Base props for a button.
*/
export interface BaseButtonProps {
readonly loading?: boolean
/**
* The variant of the button
@ -58,10 +79,18 @@ export type IconPosition = 'end' | 'start'
/**
* The variant of the button
*/
export type Variant = 'cancel' | 'custom' | 'delete' | 'icon' | 'outline' | 'primary' | 'submit'
export type Variant =
| 'cancel'
| 'custom'
| 'delete'
| 'icon'
| 'link'
| 'outline'
| 'primary'
| 'submit'
const DEFAULT_CLASSES =
'flex whitespace-nowrap cursor-pointer border border-transparent transition-[opacity,outline-offset] duration-200 ease-in-out select-none text-center items-center justify-center'
'flex whitespace-nowrap cursor-pointer border border-transparent transition-[opacity,outline-offset] duration-150 ease-in-out select-none text-center items-center justify-center'
const FOCUS_CLASSES =
'focus-visible:outline-offset-2 focus:outline-none focus-visible:outline focus-visible:outline-primary'
const EXTRA_CLICK_ZONE_CLASSES = 'flex relative before:inset-[-12px] before:absolute before:z-10'
@ -80,6 +109,7 @@ const CLASSES_FOR_SIZE: Record<Size, string> = {
const CLASSES_FOR_VARIANT: Record<Variant, string> = {
custom: '',
link: 'inline-flex px-0 py-0 rounded-sm text-primary hover:text-primary-90 hover:underline',
primary: 'bg-primary text-white hover:bg-primary-90',
cancel: 'bg-selected-frame opacity-80 hover:opacity-100',
delete: 'bg-delete text-white',
@ -103,7 +133,10 @@ const ICON_POSITION: Record<IconPosition, string> = {
}
/** A button allows a user to perform an action, with mouse, touch, and keyboard interactions. */
export function Button(props: ButtonProps) {
export const Button = React.forwardRef(function Button(
props: ButtonProps,
ref: React.ForwardedRef<HTMLButtonElement>
) {
const {
className,
children,
@ -111,22 +144,27 @@ export function Button(props: ButtonProps) {
icon,
loading = false,
isDisabled = loading,
type = 'button',
iconPosition = 'start',
size = 'xsmall',
fullWidth = false,
rounding = 'large',
...ariaButtonProps
...ariaProps
} = props
const focusChildProps = focusHooks.useFocusChild()
const isLink = ariaProps.href != null
const Tag = isLink ? aria.Link : aria.Button
const goodDefaults = isLink ? { rel: 'noopener noreferrer' } : { type: 'button' }
const classes = clsx(
DEFAULT_CLASSES,
DISABLED_CLASSES,
FOCUS_CLASSES,
CLASSES_FOR_VARIANT[variant],
CLASSES_FOR_SIZE[size],
CLASSES_FOR_ROUNDING[rounding],
CLASSES_FOR_VARIANT[variant],
{ [LOADING_CLASSES]: loading, [FULL_WIDTH_CLASSES]: fullWidth }
)
@ -150,16 +188,21 @@ export function Button(props: ButtonProps) {
}
return (
<aria.Button
{...aria.mergeProps<aria.ButtonProps>()(ariaButtonProps, focusChildProps, {
type,
<Tag
// @ts-expect-error eventhough typescript is complaining about the type of ariaProps, it is actually correct
{...aria.mergeProps()(ariaProps, focusChildProps, goodDefaults, {
ref,
isDisabled,
className: values =>
tailwindMerge.twMerge(
classes,
typeof className === 'function' ? className(values) : className
),
})}
// @ts-expect-error eventhough typescript is complaining about the type of className, it is actually correct
className={states =>
tailwindMerge.twMerge(
classes,
// this is safe, because the type of states has correct types outside
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
typeof className === 'function' ? className(states) : className
)
}
>
<aria.Text className="relative block">
<aria.Text className={clsx('block', { invisible: loading })}>{childrenFactory()}</aria.Text>
@ -170,6 +213,6 @@ export function Button(props: ButtonProps) {
</aria.Text>
)}
</aria.Text>
</aria.Button>
</Tag>
)
}
})

View File

@ -46,9 +46,8 @@ export function Dialog(props: types.DialogProps) {
isKeyboardDismissDisabled = false,
hideCloseButton = false,
className,
isOpen = false,
defaultOpen = false,
onOpenChange = () => {},
modalProps,
...ariaDialogProps
} = props
@ -60,9 +59,8 @@ export function Dialog(props: types.DialogProps) {
isDismissable={isDismissible}
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
UNSTABLE_portalContainer={root.current}
isOpen={isOpen}
onOpenChange={onOpenChange}
defaultOpen={defaultOpen}
{...modalProps}
>
<aria.Dialog
className={tailwindMerge.twMerge(DIALOG_CLASSES, [DIALOG_CLASSES_BY_TYPE[type]], className)}

View File

@ -14,8 +14,7 @@ export interface DialogProps extends aria.DialogProps {
readonly hideCloseButton?: boolean
readonly onOpenChange?: (isOpen: boolean) => void
readonly isKeyboardDismissDisabled?: boolean
readonly isOpen?: boolean
readonly defaultOpen?: boolean
readonly modalProps?: Pick<aria.ModalOverlayProps, 'className' | 'defaultOpen' | 'isOpen'>
}
/** The props for the DialogTrigger component. */

View File

@ -19,12 +19,16 @@ import * as errorModule from '#/utilities/error'
export function useToastAndLog() {
const { getText } = textProvider.useText()
const logger = loggerProvider.useLogger()
return React.useCallback(
<K extends text.TextId, T>(
textId: K | null,
...[error, ...replacements]: text.Replacements[K] extends readonly []
? [error?: errorModule.MustNotBeKnown<T>]
: [error: errorModule.MustNotBeKnown<T> | null, ...replacements: text.Replacements[K]]
? [error?: Error | errorModule.MustNotBeKnown<T>]
: [
error: Error | errorModule.MustNotBeKnown<T> | null,
...replacements: text.Replacements[K],
]
) => {
const messagePrefix =
textId == null

View File

@ -1,6 +1,8 @@
/** @file The directory header bar and directory item listing. */
import * as React from 'react'
import * as router from 'react-router-dom'
import * as appUtils from '#/appUtils'
import * as eventCallback from '#/hooks/eventCallbackHooks'
@ -328,9 +330,12 @@ export default function Drive(props: DriveProps) {
<div className={`grid grow place-items-center ${hidden ? 'hidden' : ''}`}>
<div className="flex flex-col gap-status-page text-center text-base">
{getText('upgradeToUseCloud')}
<a className="button self-center bg-help text-white" href="https://enso.org/pricing">
<router.Link
className="button self-center bg-help text-white"
to={appUtils.SUBSCRIBE_PATH}
>
{getText('upgrade')}
</a>
</router.Link>
{!supportsLocalBackend && (
<UnstyledButton
className="button self-center bg-help text-white"

View File

@ -18,6 +18,8 @@ import * as ariaComponents from '#/components/AriaComponents'
import * as backendModule from '#/services/Backend'
const PLANS_TO_SPECIFY_ORG_NAME = [backendModule.Plan.team, backendModule.Plan.enterprise]
/**
* Modal for setting the organization name.
* Shows up when the user is on the team plan and the organization name is the default.
@ -28,13 +30,21 @@ export function SetOrganizationNameModal() {
const { backend } = backendProvider.useBackend()
const { session } = authProvider.useAuth()
const userId = (session && 'user' in session && session.user?.userId) ?? null
const userPlan = (session && 'user' in session && session.user?.tier) ?? null
const userId = session && 'user' in session && session.user?.userId ? session.user.userId : null
const userPlan =
session && 'user' in session && session.user?.tier != null ? session.user.tier : null
const queryClient = reactQuery.useQueryClient()
const { data } = reactQuery.useSuspenseQuery({
queryKey: ['organization', userId],
queryFn: () => backend.getOrganization(),
queryFn: () => {
if (backend.type === backendModule.BackendType.remote) {
return backend.getOrganization().catch(() => null)
} else {
return null
}
},
staleTime: Infinity,
})
const submit = reactQuery.useMutation({
@ -49,7 +59,9 @@ export function SetOrganizationNameModal() {
})
const shouldShowModal =
userPlan === backendModule.Plan.team && data?.name?.toString() === 'Test123'
userPlan != null &&
PLANS_TO_SPECIFY_ORG_NAME.includes(userPlan) &&
data?.name?.toString() === ''
return (
<>
@ -58,7 +70,7 @@ export function SetOrganizationNameModal() {
isDismissible={false}
isKeyboardDismissDisabled
hideCloseButton
isOpen={shouldShowModal}
modalProps={{ isOpen: shouldShowModal }}
>
<aria.Form
onSubmit={e => {
@ -110,6 +122,7 @@ export function SetOrganizationNameModal() {
</ariaComponents.Button>
</aria.Form>
</ariaComponents.Dialog>
<router.Outlet context={session} />
</>
)

View File

@ -69,14 +69,9 @@ const COMPONENT_PER_PLAN: Record<backendModule.Plan, ComponentForPlan> = {
const { getText } = textProvider.useText()
return (
<a
href="https://enso.org/pricing"
target="_blank"
referrerPolicy="no-referrer"
className="underline"
>
<ariaComponents.Button variant="link" href="https://enso.org/pricing" target="_blank">
{getText('learnMore')}
</a>
</ariaComponents.Button>
)
},
pricing: 'soloPlanPricing',
@ -105,14 +100,9 @@ const COMPONENT_PER_PLAN: Record<backendModule.Plan, ComponentForPlan> = {
const { getText } = textProvider.useText()
return (
<a
href="https://enso.org/pricing"
target="_blank"
referrerPolicy="no-referrer"
className="underline"
>
<ariaComponents.Button variant="link" href="https://enso.org/pricing" target="_blank">
{getText('learnMore')}
</a>
</ariaComponents.Button>
)
},
pricing: 'teamPlanPricing',
@ -141,14 +131,9 @@ const COMPONENT_PER_PLAN: Record<backendModule.Plan, ComponentForPlan> = {
const { getText } = textProvider.useText()
return (
<a
href="https://enso.org/pricing"
target="_blank"
referrerPolicy="no-referrer"
className="underline"
>
<ariaComponents.Button variant="link" href="https://enso.org/pricing" target="_blank">
{getText('learnMore')}
</a>
</ariaComponents.Button>
)
},
pricing: 'enterprisePlanPricing',
@ -158,7 +143,14 @@ const COMPONENT_PER_PLAN: Record<backendModule.Plan, ComponentForPlan> = {
submitButton: () => {
const { getText } = textProvider.useText()
return (
<ariaComponents.Button variant="primary" fullWidth size="medium" rounding="full">
<ariaComponents.Button
fullWidth
variant="primary"
size="medium"
rounding="full"
target="_blank"
href="mailto:contact@enso.org?subject=Upgrading%20to%20Organization%20Plan"
>
{getText('contactSales')}
</ariaComponents.Button>
)

View File

@ -158,8 +158,14 @@ declare module 'create-servers' {
/** Configuration options for `create-servers`. */
interface CreateServersOptions {
readonly http: number
readonly http?: number
readonly handler: http.RequestListener
// eslint-disable-next-line no-restricted-syntax
readonly https?: {
readonly port: number
readonly key: string
readonly cert: string
}
}
/** An error passed to a callback when a HTTP request fails. */

32
package-lock.json generated
View File

@ -181,6 +181,7 @@
"create-servers": "3.2.0",
"electron-is-dev": "^2.0.0",
"mime-types": "^2.1.35",
"mkcert": "3.2.0",
"opener": "^1.5.2",
"string-length": "^5.0.1",
"tar": "^6.1.13",
@ -14915,6 +14916,29 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/mkcert": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mkcert/-/mkcert-3.2.0.tgz",
"integrity": "sha512-026Eivq9RoOjOuLJGzbhGwXUAjBxRX11Z7Jbm4/7lqT/Av+XNy9SPrJte6+UpEt7i+W3e/HZYxQqlQcqXZWSzg==",
"dependencies": {
"commander": "^11.0.0",
"node-forge": "^1.3.1"
},
"bin": {
"mkcert": "dist/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/mkcert/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -15133,6 +15157,14 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/node-gyp-build": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz",