mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:41:34 +03:00
Frontend adjustments for Stripe refactoring (#10054)
#### Tl;dr Closes: enso-org/cloud-v2#1185 This PR changes endpoints according to the comment left by @PabloBuchu: https://github.com/enso-org/cloud-v2/issues/1185#issuecomment-2114606101 #### This Change: Changes endpoint names and fixes subscription flow ---
This commit is contained in:
parent
06327f8fde
commit
28655e13d7
@ -17,6 +17,7 @@
|
||||
<body>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<div id="enso-chat" class="enso-chat"></div>
|
||||
<div id="enso-portal-root" class="enso-portal-root"></div>
|
||||
<script type="module" src="/src/entrypoint.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -39,6 +39,7 @@
|
||||
<div id="app"></div>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<div id="enso-chat" class="enso-chat"></div>
|
||||
<div id="enso-portal-root" class="enso-portal-root"></div>
|
||||
<noscript>
|
||||
This page requires JavaScript to run. Please enable it in your browser.
|
||||
</noscript>
|
||||
|
@ -59,8 +59,10 @@ export async function mockApi({ page }: MockParams) {
|
||||
rootDirectoryId: defaultDirectoryId,
|
||||
userGroups: null,
|
||||
}
|
||||
|
||||
let currentUser: backend.User | null = defaultUser
|
||||
let currentOrganization: backend.OrganizationInfo | null = null
|
||||
|
||||
const assetMap = new Map<backend.AssetId, backend.AnyAsset>()
|
||||
const deletedAssets = new Set<backend.AssetId>()
|
||||
const assets: backend.AnyAsset[] = []
|
||||
|
@ -40,6 +40,7 @@
|
||||
<div id="app"></div>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<div id="enso-chat" class="enso-chat"></div>
|
||||
<div id="enso-portal-root" class="enso-portal-root"></div>
|
||||
<noscript>
|
||||
This page requires JavaScript to run. Please enable it in your browser.
|
||||
</noscript>
|
||||
|
@ -149,6 +149,7 @@ export interface AppProps {
|
||||
readonly projectManagerUrl: string | null
|
||||
readonly ydocUrl: string | null
|
||||
readonly appRunner: types.EditorRunner | null
|
||||
readonly portalRoot: Element
|
||||
}
|
||||
|
||||
/** Component called by the parent module, returning the root React component for this
|
||||
@ -222,6 +223,7 @@ export interface AppRouterProps extends AppProps {
|
||||
function AppRouter(props: AppRouterProps) {
|
||||
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
|
||||
const { onAuthenticated, projectManagerUrl, projectManagerRootDirectory } = props
|
||||
const { portalRoot } = props
|
||||
// `navigateHooks.useNavigate` cannot be used here as it relies on `AuthProvider`, which has not
|
||||
// yet been initialized at this point.
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
@ -234,9 +236,6 @@ function AppRouter(props: AppRouterProps) {
|
||||
window.navigate = navigate
|
||||
}
|
||||
const [inputBindingsRaw] = React.useState(() => inputBindingsModule.createBindings())
|
||||
const [root] = React.useState<React.RefObject<HTMLElement>>(() => ({
|
||||
current: document.getElementById('enso-dashboard'),
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
const savedInputBindings = localStorage.get('inputBindings')
|
||||
@ -488,7 +487,7 @@ function AppRouter(props: AppRouterProps) {
|
||||
)
|
||||
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
|
||||
result = (
|
||||
<rootComponent.Root rootRef={root} navigate={navigate}>
|
||||
<rootComponent.Root navigate={navigate} portalRoot={portalRoot}>
|
||||
{result}
|
||||
</rootComponent.Root>
|
||||
)
|
||||
|
@ -45,6 +45,8 @@ const DIALOG_STYLES = twv.tv({
|
||||
},
|
||||
})
|
||||
|
||||
const IGNORE_INTERACT_OUTSIDE_ELEMENTS = ['Toastify__toast-container', 'tsqd-parent-container']
|
||||
|
||||
// ==============
|
||||
// === Dialog ===
|
||||
// ==============
|
||||
@ -65,6 +67,7 @@ export function Dialog(props: types.DialogProps) {
|
||||
testId = 'dialog',
|
||||
...ariaDialogProps
|
||||
} = props
|
||||
const shouldCloseOnInteractOutsideRef = React.useRef(false)
|
||||
const dialogRef = React.useRef<HTMLDivElement>(null)
|
||||
const overlayState = React.useRef<aria.OverlayTriggerState | null>(null)
|
||||
|
||||
@ -74,17 +77,32 @@ export function Dialog(props: types.DialogProps) {
|
||||
|
||||
aria.useInteractOutside({
|
||||
ref: dialogRef,
|
||||
onInteractOutside: () => {
|
||||
if (isDismissable) {
|
||||
overlayState.current?.close()
|
||||
} else {
|
||||
const duration = 200 // 200ms
|
||||
// we need to prevent the dialog from closing when interacting with the toastify container
|
||||
// and when interaction starts, we check if the target is inside the toastify container
|
||||
// and in the next callback we prevent the dialog from closing
|
||||
// For some reason aria doesn't fire onInteractOutsideStart if onInteractOutside is not defined
|
||||
onInteractOutsideStart: e => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
dialogRef.current?.animate(
|
||||
[{ transform: 'scale(1)' }, { transform: 'scale(1.015)' }, { transform: 'scale(1)' }],
|
||||
{ duration, iterations: 1, direction: 'alternate' }
|
||||
)
|
||||
shouldCloseOnInteractOutsideRef.current = !IGNORE_INTERACT_OUTSIDE_ELEMENTS.some(selector =>
|
||||
target.closest(`.${selector}`)
|
||||
)
|
||||
},
|
||||
onInteractOutside: () => {
|
||||
if (shouldCloseOnInteractOutsideRef.current) {
|
||||
if (isDismissable) {
|
||||
overlayState.current?.close()
|
||||
} else {
|
||||
const duration = 200 // 200ms
|
||||
dialogRef.current?.animate(
|
||||
[{ transform: 'scale(1)' }, { transform: 'scale(1.015)' }, { transform: 'scale(1)' }],
|
||||
{ duration, iterations: 1, direction: 'alternate' }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
shouldCloseOnInteractOutsideRef.current = false
|
||||
},
|
||||
})
|
||||
|
||||
@ -93,8 +111,9 @@ export function Dialog(props: types.DialogProps) {
|
||||
className={OVERLAY_STYLES}
|
||||
isDismissable={isDismissable}
|
||||
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
||||
UNSTABLE_portalContainer={root.current}
|
||||
UNSTABLE_portalContainer={root}
|
||||
onOpenChange={onOpenChange}
|
||||
shouldCloseOnInteractOutside={() => false}
|
||||
{...modalProps}
|
||||
>
|
||||
{values => {
|
||||
@ -105,8 +124,9 @@ export function Dialog(props: types.DialogProps) {
|
||||
className={MODAL_STYLES}
|
||||
isDismissable={isDismissable}
|
||||
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
||||
UNSTABLE_portalContainer={root.current}
|
||||
UNSTABLE_portalContainer={root}
|
||||
onOpenChange={onOpenChange}
|
||||
shouldCloseOnInteractOutside={() => false}
|
||||
{...modalProps}
|
||||
>
|
||||
<aria.Dialog
|
||||
|
@ -59,7 +59,7 @@ export function Popover(props: PopoverProps) {
|
||||
className: typeof className === 'function' ? className(values) : className,
|
||||
})
|
||||
}
|
||||
UNSTABLE_portalContainer={root.current}
|
||||
UNSTABLE_portalContainer={root}
|
||||
{...ariaPopoverProps}
|
||||
>
|
||||
{opts => (
|
||||
|
@ -71,7 +71,7 @@ export function Tooltip(props: TooltipProps) {
|
||||
<aria.Tooltip
|
||||
offset={DEFAULT_OFFSET}
|
||||
containerPadding={containerPadding}
|
||||
UNSTABLE_portalContainer={root.current}
|
||||
UNSTABLE_portalContainer={root}
|
||||
className={aria.composeRenderProps(className, (classNames, values) =>
|
||||
TOOLTIP_STYLES({ className: classNames, ...values })
|
||||
)}
|
||||
|
@ -6,7 +6,7 @@ import * as React from 'react'
|
||||
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
const PortalContext = React.createContext<React.RefObject<Element | null> | null>(null)
|
||||
const PortalContext = React.createContext<Element | null>(null)
|
||||
|
||||
/**
|
||||
* Allows to access the root element for the Portal component
|
||||
@ -24,15 +24,9 @@ export function usePortalContext() {
|
||||
export function useStrictPortalContext() {
|
||||
const root = React.useContext(PortalContext)
|
||||
|
||||
invariant(
|
||||
root != null && root.current != null,
|
||||
'You should use `PortalProvider` to access the `Portal` component'
|
||||
)
|
||||
invariant(root != null, 'You should use `PortalProvider` to access the `Portal` component')
|
||||
|
||||
// This is safe because we are using `invariant` to check if the `PortalProvider` is in the component tree
|
||||
// and the root element is not `null`
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return root as { current: Element }
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,14 +25,13 @@ export function usePortal(props: types.PortalProps) {
|
||||
if (!isDisabled) {
|
||||
const contextRoot = portalContext.root
|
||||
const currentRoot = root?.current ?? null
|
||||
const currentContextRoot = contextRoot?.current ?? null
|
||||
|
||||
invariant(
|
||||
!(contextRoot == null && currentRoot == null),
|
||||
'Before using Portal, you need to specify a root, where the component should be mounted or put the component under the <Root /> component'
|
||||
)
|
||||
|
||||
setMountRoot(currentRoot ?? currentContextRoot)
|
||||
setMountRoot(currentRoot ?? contextRoot)
|
||||
}
|
||||
}, [root, portalContext.root, isDisabled])
|
||||
|
||||
|
@ -10,17 +10,17 @@ import * as portal from '#/components/Portal'
|
||||
|
||||
/** Props for {@link Root}. */
|
||||
export interface RootProps extends React.PropsWithChildren {
|
||||
readonly rootRef: React.RefObject<HTMLElement>
|
||||
readonly portalRoot: Element
|
||||
readonly navigate: (path: string) => void
|
||||
readonly locale?: string
|
||||
}
|
||||
|
||||
/** The root component with required providers. */
|
||||
export function Root(props: RootProps) {
|
||||
const { children, rootRef, navigate, locale = 'en-US' } = props
|
||||
const { children, navigate, locale = 'en-US', portalRoot } = props
|
||||
|
||||
return (
|
||||
<portal.PortalProvider value={rootRef}>
|
||||
<portal.PortalProvider value={portalRoot}>
|
||||
<aria.RouterProvider navigate={navigate}>
|
||||
<aria.I18nProvider locale={locale}>{children}</aria.I18nProvider>
|
||||
</aria.RouterProvider>
|
||||
|
@ -16,6 +16,7 @@ import type * as geometry from '#/utilities/geometry'
|
||||
|
||||
/** Props for a {@link SelectionBrush}. */
|
||||
export interface SelectionBrushProps {
|
||||
readonly targetRef: React.RefObject<HTMLElement>
|
||||
readonly onDrag: (rectangle: geometry.DetailedRectangle, event: MouseEvent) => void
|
||||
readonly onDragEnd: (event: MouseEvent) => void
|
||||
readonly onDragCancel: () => void
|
||||
@ -23,7 +24,7 @@ export interface SelectionBrushProps {
|
||||
|
||||
/** A selection brush to indicate the area being selected by the mouse drag action. */
|
||||
export default function SelectionBrush(props: SelectionBrushProps) {
|
||||
const { onDrag, onDragEnd, onDragCancel } = props
|
||||
const { onDrag, onDragEnd, onDragCancel, targetRef } = props
|
||||
const { modalRef } = modalProvider.useModalRef()
|
||||
const isMouseDownRef = React.useRef(false)
|
||||
const didMoveWhileDraggingRef = React.useRef(false)
|
||||
@ -61,70 +62,77 @@ export default function SelectionBrush(props: SelectionBrushProps) {
|
||||
}, [anchorAnimFactor, anchor])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
if (
|
||||
modalRef.current == null &&
|
||||
!eventModule.isElementTextInput(event.target) &&
|
||||
!(event.target instanceof HTMLButtonElement) &&
|
||||
!(event.target instanceof HTMLAnchorElement)
|
||||
) {
|
||||
isMouseDownRef.current = true
|
||||
didMoveWhileDraggingRef.current = false
|
||||
lastMouseEvent.current = event
|
||||
const newAnchor = { left: event.pageX, top: event.pageY }
|
||||
setAnchor(newAnchor)
|
||||
setLastSetAnchor(newAnchor)
|
||||
setPosition(newAnchor)
|
||||
if (targetRef.current != null) {
|
||||
const target = targetRef.current
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
if (
|
||||
modalRef.current == null &&
|
||||
!eventModule.isElementTextInput(event.target) &&
|
||||
!(event.target instanceof HTMLButtonElement) &&
|
||||
!(event.target instanceof HTMLAnchorElement)
|
||||
) {
|
||||
isMouseDownRef.current = true
|
||||
didMoveWhileDraggingRef.current = false
|
||||
lastMouseEvent.current = event
|
||||
const newAnchor = { left: event.pageX, top: event.pageY }
|
||||
setAnchor(newAnchor)
|
||||
setLastSetAnchor(newAnchor)
|
||||
setPosition(newAnchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
const onMouseUp = (event: MouseEvent) => {
|
||||
if (didMoveWhileDraggingRef.current) {
|
||||
onDragEndRef.current(event)
|
||||
}
|
||||
// The `setTimeout` is required, otherwise the values are changed before the `onClick` handler
|
||||
// is executed.
|
||||
window.setTimeout(() => {
|
||||
isMouseDownRef.current = false
|
||||
didMoveWhileDraggingRef.current = false
|
||||
})
|
||||
setAnchor(null)
|
||||
}
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!(event.buttons & 1)) {
|
||||
isMouseDownRef.current = false
|
||||
}
|
||||
if (isMouseDownRef.current) {
|
||||
// Left click is being held.
|
||||
didMoveWhileDraggingRef.current = true
|
||||
lastMouseEvent.current = event
|
||||
setPosition({ left: event.pageX, top: event.pageY })
|
||||
}
|
||||
}
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (isMouseDownRef.current && didMoveWhileDraggingRef.current) {
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
const onDragStart = () => {
|
||||
if (isMouseDownRef.current) {
|
||||
isMouseDownRef.current = false
|
||||
onDragCancelRef.current()
|
||||
const onMouseUp = (event: MouseEvent) => {
|
||||
if (didMoveWhileDraggingRef.current) {
|
||||
onDragEndRef.current(event)
|
||||
}
|
||||
// The `setTimeout` is required, otherwise the values are changed before the `onClick` handler
|
||||
// is executed.
|
||||
window.setTimeout(() => {
|
||||
isMouseDownRef.current = false
|
||||
didMoveWhileDraggingRef.current = false
|
||||
})
|
||||
setAnchor(null)
|
||||
}
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!(event.buttons & 1)) {
|
||||
isMouseDownRef.current = false
|
||||
}
|
||||
if (isMouseDownRef.current) {
|
||||
// Left click is being held.
|
||||
didMoveWhileDraggingRef.current = true
|
||||
lastMouseEvent.current = event
|
||||
setPosition({ left: event.pageX, top: event.pageY })
|
||||
}
|
||||
}
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (isMouseDownRef.current && didMoveWhileDraggingRef.current) {
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
const onDragStart = () => {
|
||||
if (isMouseDownRef.current) {
|
||||
isMouseDownRef.current = false
|
||||
onDragCancelRef.current()
|
||||
setAnchor(null)
|
||||
}
|
||||
}
|
||||
|
||||
target.addEventListener('mousedown', onMouseDown)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('dragstart', onDragStart, { capture: true })
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('click', onClick, { capture: true })
|
||||
|
||||
return () => {
|
||||
target.removeEventListener('mousedown', onMouseDown)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('dragstart', onDragStart, { capture: true })
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('click', onClick, { capture: true })
|
||||
}
|
||||
} else {
|
||||
return () => {}
|
||||
}
|
||||
document.addEventListener('mousedown', onMouseDown)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('dragstart', onDragStart, { capture: true })
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('click', onClick, { capture: true })
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onMouseDown)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('dragstart', onDragStart, { capture: true })
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('click', onClick, { capture: true })
|
||||
}
|
||||
}, [/* should never change */ modalRef])
|
||||
}, [/* should never change */ modalRef, targetRef])
|
||||
|
||||
const rectangle = React.useMemo(() => {
|
||||
if (position != null && lastSetAnchor != null) {
|
||||
|
@ -7,6 +7,7 @@ import * as sentry from '@sentry/react'
|
||||
import * as reactQuery from '@tanstack/react-query'
|
||||
import * as reactDOM from 'react-dom/client'
|
||||
import * as reactRouter from 'react-router-dom'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
@ -39,9 +40,11 @@ const SENTRY_SAMPLE_RATE = 0.005
|
||||
export // This export declaration must be broken up to satisfy the `require-jsdoc` rule.
|
||||
// This is not a React component even though it contains JSX.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
function run(props: app.AppProps) {
|
||||
function run(props: Omit<app.AppProps, 'portalRoot'>) {
|
||||
const { logger, vibrancy, supportsDeepLinks } = props
|
||||
|
||||
logger.log('Starting authentication/dashboard UI.')
|
||||
|
||||
if (
|
||||
!detect.IS_DEV_MODE &&
|
||||
process.env.ENSO_CLOUD_SENTRY_DSN != null &&
|
||||
@ -75,32 +78,36 @@ function run(props: app.AppProps) {
|
||||
document.body.classList.add('vibrancy')
|
||||
}
|
||||
|
||||
/** The root element into which the authentication/dashboard app will be rendered. */
|
||||
const root = document.getElementById(ROOT_ELEMENT_ID)
|
||||
if (root == null) {
|
||||
logger.error(`Could not find root element with ID '${ROOT_ELEMENT_ID}'.`)
|
||||
} else {
|
||||
// `supportsDeepLinks` will be incorrect when accessing the installed Electron app's pages
|
||||
// via the browser.
|
||||
const actuallySupportsDeepLinks = supportsDeepLinks && detect.isOnElectron()
|
||||
const queryClient = reactQueryClientModule.createReactQueryClient()
|
||||
const portalRoot = document.querySelector('#enso-portal-root')
|
||||
|
||||
reactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<reactQuery.QueryClientProvider client={queryClient}>
|
||||
<errorBoundary.ErrorBoundary>
|
||||
<React.Suspense fallback={<LoadingScreen />}>
|
||||
{detect.IS_DEV_MODE ? (
|
||||
<App {...props} />
|
||||
) : (
|
||||
<App {...props} supportsDeepLinks={actuallySupportsDeepLinks} />
|
||||
)}
|
||||
</React.Suspense>
|
||||
</errorBoundary.ErrorBoundary>
|
||||
</reactQuery.QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
invariant(root, 'Root element not found')
|
||||
invariant(portalRoot, 'PortalRoot element not found')
|
||||
|
||||
// `supportsDeepLinks` will be incorrect when accessing the installed Electron app's pages
|
||||
// via the browser.
|
||||
const actuallySupportsDeepLinks = supportsDeepLinks && detect.isOnElectron()
|
||||
const queryClient = reactQueryClientModule.createReactQueryClient()
|
||||
|
||||
reactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<reactQuery.QueryClientProvider client={queryClient}>
|
||||
<errorBoundary.ErrorBoundary>
|
||||
<React.Suspense fallback={<LoadingScreen />}>
|
||||
{detect.IS_DEV_MODE ? (
|
||||
<App {...props} portalRoot={portalRoot} />
|
||||
) : (
|
||||
<App
|
||||
{...props}
|
||||
supportsDeepLinks={actuallySupportsDeepLinks}
|
||||
portalRoot={portalRoot}
|
||||
/>
|
||||
)}
|
||||
</React.Suspense>
|
||||
</errorBoundary.ErrorBoundary>
|
||||
</reactQuery.QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
/** Global configuration for the {@link App} component. */
|
||||
|
@ -2536,6 +2536,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
{!hidden && hiddenContextMenu}
|
||||
{!hidden && (
|
||||
<SelectionBrush
|
||||
targetRef={rootRef}
|
||||
onDrag={onSelectionDrag}
|
||||
onDragEnd={onSelectionDragEnd}
|
||||
onDragCancel={onSelectionDragCancel}
|
||||
|
@ -332,24 +332,26 @@ export default function Drive(props: DriveProps) {
|
||||
testId="not-enabled-stub"
|
||||
subtitle={`${getText('notEnabledSubtitle')}${!supportsLocalBackend ? ' ' + getText('downloadFreeEditionMessage') : ''}`}
|
||||
>
|
||||
{!supportsLocalBackend && (
|
||||
<ariaComponents.Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
rounded="full"
|
||||
data-testid="download-free-edition"
|
||||
onPress={async () => {
|
||||
const downloadUrl = await github.getDownloadUrl()
|
||||
if (downloadUrl == null) {
|
||||
toastAndLog('noAppDownloadError')
|
||||
} else {
|
||||
download.download(downloadUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getText('downloadFreeEdition')}
|
||||
</ariaComponents.Button>
|
||||
)}
|
||||
<ariaComponents.ButtonGroup align="center">
|
||||
{!supportsLocalBackend && (
|
||||
<ariaComponents.Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
rounded="full"
|
||||
data-testid="download-free-edition"
|
||||
onPress={async () => {
|
||||
const downloadUrl = await github.getDownloadUrl()
|
||||
if (downloadUrl == null) {
|
||||
toastAndLog('noAppDownloadError')
|
||||
} else {
|
||||
download.download(downloadUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getText('downloadFreeEdition')}
|
||||
</ariaComponents.Button>
|
||||
)}
|
||||
</ariaComponents.ButtonGroup>
|
||||
</result.Result>
|
||||
)
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ export default function Settings() {
|
||||
const root = portal.useStrictPortalContext()
|
||||
const [isUserInOrganization, setIsUserInOrganization] = React.useState(true)
|
||||
const [isSidebarPopoverOpen, setIsSidebarPopoverOpen] = React.useState(false)
|
||||
|
||||
const [organization, setOrganization] = React.useState<backendModule.OrganizationInfo>(() => ({
|
||||
id: user?.organizationId ?? backendModule.OrganizationId(''),
|
||||
name: null,
|
||||
@ -55,6 +56,7 @@ export default function Settings() {
|
||||
website: null,
|
||||
address: null,
|
||||
picture: null,
|
||||
subscription: {},
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -112,7 +114,7 @@ export default function Settings() {
|
||||
<aria.Heading level={1} className="flex h-heading px-heading-x text-xl font-bold">
|
||||
<aria.MenuTrigger isOpen={isSidebarPopoverOpen} onOpenChange={setIsSidebarPopoverOpen}>
|
||||
<Button image={BurgerMenuIcon} buttonClassName="mr-3 sm:hidden" onPress={() => {}} />
|
||||
<aria.Popover UNSTABLE_portalContainer={root.current}>
|
||||
<aria.Popover UNSTABLE_portalContainer={root}>
|
||||
<SettingsSidebar
|
||||
isMenu
|
||||
isUserInOrganization={isUserInOrganization}
|
||||
|
@ -99,7 +99,7 @@ export function SetOrganizationNameModal() {
|
||||
</aria.TextField>
|
||||
|
||||
{submit.error && (
|
||||
<ariaComponents.Alert variant="error" size="medium">
|
||||
<ariaComponents.Alert variant="error" size="medium" className="mt-4">
|
||||
{submit.error.message}
|
||||
</ariaComponents.Alert>
|
||||
)}
|
||||
|
@ -90,7 +90,7 @@ export function Subscribe() {
|
||||
return backend.getCheckoutSession(id)
|
||||
},
|
||||
onSuccess: (data, mutationData) => {
|
||||
if (data.status === 'complete') {
|
||||
if (['trialing', 'active'].includes(data.status)) {
|
||||
navigate({ pathname: appUtils.SUBSCRIBE_SUCCESS_PATH, search: `plan=${mutationData.plan}` })
|
||||
return
|
||||
} else {
|
||||
|
@ -36,15 +36,17 @@ export function SubscribeSuccess() {
|
||||
subtitle={getText('subscribeSuccessSubtitle', getText(constants.PLAN_TO_TEXT_ID[plan]))}
|
||||
status="success"
|
||||
>
|
||||
<ariaComponents.Button
|
||||
variant="submit"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
navigate(appUtils.DASHBOARD_PATH)
|
||||
}}
|
||||
>
|
||||
{getText('subscribeSuccessSubmit')}
|
||||
</ariaComponents.Button>
|
||||
<ariaComponents.ButtonGroup align="center">
|
||||
<ariaComponents.Button
|
||||
variant="submit"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
navigate(appUtils.DASHBOARD_PATH)
|
||||
}}
|
||||
>
|
||||
{getText('subscribeSuccessSubmit')}
|
||||
</ariaComponents.Button>
|
||||
</ariaComponents.ButtonGroup>
|
||||
</result.Result>
|
||||
)
|
||||
} else {
|
||||
|
@ -66,6 +66,12 @@ export type AssetId = IdType[keyof IdType]
|
||||
export type CheckoutSessionId = newtype.Newtype<string, 'CheckoutSessionId'>
|
||||
export const CheckoutSessionId = newtype.newtypeConstructor<CheckoutSessionId>()
|
||||
|
||||
/**
|
||||
* Unique identifier for a subscription.
|
||||
*/
|
||||
export type SubscriptionId = newtype.Newtype<string, 'SubscriptionId'>
|
||||
export const SubscriptionId = newtype.newtypeConstructor<SubscriptionId>()
|
||||
|
||||
/** The name of an asset label. */
|
||||
export type LabelName = newtype.Newtype<string, 'LabelName'>
|
||||
export const LabelName = newtype.newtypeConstructor<LabelName>()
|
||||
@ -442,6 +448,16 @@ export interface ResourceUsage {
|
||||
readonly storage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a subscription.
|
||||
*/
|
||||
export interface Subscription {
|
||||
readonly id?: SubscriptionId
|
||||
readonly plan?: Plan
|
||||
readonly trialStart?: dateTime.Rfc3339DateTime | null
|
||||
readonly trialEnd?: dateTime.Rfc3339DateTime | null
|
||||
}
|
||||
|
||||
/** Metadata for an organization. */
|
||||
export interface OrganizationInfo {
|
||||
readonly id: OrganizationId
|
||||
@ -450,6 +466,7 @@ export interface OrganizationInfo {
|
||||
readonly website: HttpsUrl | null
|
||||
readonly address: string | null
|
||||
readonly picture: HttpsUrl | null
|
||||
readonly subscription: Subscription
|
||||
}
|
||||
|
||||
/** A user group and its associated metadata. */
|
||||
|
@ -64,9 +64,10 @@ export const LIST_USER_GROUPS_PATH = 'usergroups'
|
||||
/** Relative HTTP path to the "list versions" endpoint of the Cloud backend API. */
|
||||
export const LIST_VERSIONS_PATH = 'versions'
|
||||
/** Relative HTTP path to the "create checkout session" endpoint of the Cloud backend API. */
|
||||
export const CREATE_CHECKOUT_SESSION_PATH = 'payments/checkout-sessions'
|
||||
export const CREATE_CHECKOUT_SESSION_PATH = 'payments/subscriptions'
|
||||
/** Relative HTTP path to the "get checkout session" endpoint of the Cloud backend API. */
|
||||
export const GET_CHECKOUT_SESSION_PATH = 'payments/checkout-sessions'
|
||||
export const GET_CHECKOUT_SESSION_PATH = 'payments/subscriptions'
|
||||
export const CANCEL_SUBSCRIPTION_PATH = 'payments/subscription'
|
||||
/** Relative HTTP path to the "get log events" endpoint of the Cloud backend API. */
|
||||
export const GET_LOG_EVENTS_PATH = 'log_events'
|
||||
/** Relative HTTP path to the "post log event" endpoint of the Cloud backend API. */
|
||||
@ -157,5 +158,5 @@ export function deleteUserGroupPath(groupId: backend.UserGroupId) {
|
||||
}
|
||||
/** Relative HTTP path to the "get checkout session" endpoint of the Cloud backend API. */
|
||||
export function getCheckoutSessionPath(checkoutSessionId: backend.CheckoutSessionId) {
|
||||
return `payments/checkout-sessions/${checkoutSessionId}`
|
||||
return `${GET_CHECKOUT_SESSION_PATH}/${checkoutSessionId}`
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@400;500;600;700&display=swap");
|
||||
@import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
/* Scoped to `.enso-dashboard, .enso-chat` in tailwind config. */
|
||||
/* Scoped to `.enso-dashboard, .enso-chat, .enso-portal-root` in tailwind config. */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@ -483,8 +483,7 @@ body::before {
|
||||
|
||||
/* These styles MUST still be copied
|
||||
* as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */
|
||||
.enso-dashboard,
|
||||
.enso-chat {
|
||||
:where(:is(.enso-dashboard, .enso-chat, .enso-portal-root)) {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-tab-size: 4;
|
||||
@ -494,16 +493,20 @@ body::before {
|
||||
font-feature-settings: normal;
|
||||
}
|
||||
|
||||
.enso-dashboard kbd,
|
||||
.enso-chat kbd {
|
||||
:where(:is(.enso-dashboard kbd, .enso-chat kbd, .enso-portal-root kbd)) {
|
||||
font-family: "M PLUS 1";
|
||||
}
|
||||
|
||||
.enso-dashboard :focus,
|
||||
.enso-chat :focus {
|
||||
:where(
|
||||
:is(.enso-dashboard :focus, .enso-chat :focus, .enso-portal-root :focus)
|
||||
) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:where(.enso-portal-root) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Must be kept in sync with app/gui/view/graph-editor/src/builtin/visualization/java_script/helpers/scrollable.js. */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@ -7,7 +7,7 @@ import plugin from 'tailwindcss/plugin.js'
|
||||
/* eslint-disable no-restricted-syntax, @typescript-eslint/naming-convention, @typescript-eslint/no-magic-numbers */
|
||||
export default /** @satisfies {import('tailwindcss').Config} */ ({
|
||||
content: ['./src/**/*.tsx', './src/**/*.ts'],
|
||||
important: `:is(.enso-dashboard, .enso-chat)`,
|
||||
important: `:is(.enso-dashboard, .enso-chat, .enso-portal-root)`,
|
||||
theme: {
|
||||
extend: {
|
||||
cursor: {
|
||||
|
Loading…
Reference in New Issue
Block a user