Add prefixes for cloud url params (#9649)

This PR adds an ability to exclude some keys in URLSearchParams from being parsed by GUI.
This commit is contained in:
Sergei Garin 2024-04-09 10:38:06 +03:00 committed by GitHub
parent fc557f8fd2
commit 2c78f4eefd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 7 deletions

View File

@ -11,6 +11,14 @@ async function runApp(
_metadata?: object | undefined, _metadata?: object | undefined,
pinia?: Pinia | undefined, pinia?: Pinia | undefined,
) { ) {
const ignoreParamsRegex = (() => {
if (_metadata)
if ('ignoreParamsRegex' in _metadata)
if (_metadata['ignoreParamsRegex'] instanceof RegExp) return _metadata['ignoreParamsRegex']
return null
})()
running = true running = true
const { mountProjectApp } = await import('./createApp') const { mountProjectApp } = await import('./createApp')
if (!running) return if (!running) return
@ -19,7 +27,11 @@ async function runApp(
function onUnrecognizedOption(path: string[]) { function onUnrecognizedOption(path: string[]) {
unrecognizedOptions.push(path.join('.')) unrecognizedOptions.push(path.join('.'))
} }
const intermediateConfig = mergeConfig(baseConfig, urlParams(), { onUnrecognizedOption }) const intermediateConfig = mergeConfig(
baseConfig,
urlParams({ ignoreKeysRegExp: ignoreParamsRegex }),
{ onUnrecognizedOption },
)
const appConfig = mergeConfig(intermediateConfig, config ?? {}) const appConfig = mergeConfig(intermediateConfig, config ?? {})
unmount = await mountProjectApp({ config: appConfig, accessToken, unrecognizedOptions }, pinia) unmount = await mountProjectApp({ config: appConfig, accessToken, unrecognizedOptions }, pinia)
} }

View File

@ -1,13 +1,25 @@
import type { StringConfig } from './config' import type { StringConfig } from './config'
export interface UrlParamsProps {
ignoreKeysRegExp?: RegExp | null
}
/** Returns the parameters passed in the URL query string. */ /** Returns the parameters passed in the URL query string. */
export function urlParams(): StringConfig { export function urlParams(props: UrlParamsProps = {}): StringConfig {
const { ignoreKeysRegExp } = props
const params: StringConfig = {} const params: StringConfig = {}
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
for (const [name, value] of urlParams.entries()) { for (const [name, value] of urlParams.entries()) {
let obj = params let obj = params
const path = name.split('.') const path = name.split('.')
const lastSegment = path.pop() const lastSegment = path.pop()
if (ignoreKeysRegExp != null && ignoreKeysRegExp.test(name)) {
continue
}
if (lastSegment == null) { if (lastSegment == null) {
console.error(`Invalid URL parameter name: '${name}'`) console.error(`Invalid URL parameter name: '${name}'`)
} else { } else {

View File

@ -4,6 +4,10 @@
// === Constants === // === Constants ===
// ================= // =================
// =============
// === Paths ===
// =============
/** Path to the root of the app (i.e., the Cloud dashboard). */ /** Path to the root of the app (i.e., the Cloud dashboard). */
export const DASHBOARD_PATH = '/' export const DASHBOARD_PATH = '/'
/** Path to the login page. */ /** Path to the login page. */
@ -28,3 +32,9 @@ export const ALL_PATHS_REGEX = new RegExp(
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|` + `${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|` +
`${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH})$` `${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH})$`
) )
// ===========
// === URL ===
// ===========
export const SEARCH_PARAMS_PREFIX = 'cloud-ide_'

View File

@ -7,6 +7,8 @@ import * as React from 'react'
import * as reactRouterDom from 'react-router-dom' import * as reactRouterDom from 'react-router-dom'
import * as appUtils from '#/appUtils'
import * as eventCallback from '#/hooks/eventCallbackHooks' import * as eventCallback from '#/hooks/eventCallbackHooks'
import * as lazyMemo from '#/hooks/useLazyMemoHooks' import * as lazyMemo from '#/hooks/useLazyMemoHooks'
@ -32,22 +34,31 @@ export function useSearchParamsState<T = unknown>(
): SearchParamsStateReturnType<T> { ): SearchParamsStateReturnType<T> {
const [searchParams, setSearchParams] = reactRouterDom.useSearchParams() const [searchParams, setSearchParams] = reactRouterDom.useSearchParams()
const prefixedKey = `${appUtils.SEARCH_PARAMS_PREFIX}${key}`
const lazyDefaultValueInitializer = lazyMemo.useLazyMemoHooks(defaultValue, []) const lazyDefaultValueInitializer = lazyMemo.useLazyMemoHooks(defaultValue, [])
const predicateEventCallback = eventCallback.useEventCallback(predicate) const predicateEventCallback = eventCallback.useEventCallback(predicate)
const clear = eventCallback.useEventCallback((replace: boolean = false) => { const clear = eventCallback.useEventCallback((replace: boolean = false) => {
searchParams.delete(key) searchParams.delete(prefixedKey)
setSearchParams(searchParams, { replace }) setSearchParams(searchParams, { replace })
}) })
const unprefixedValue = searchParams.get(key)
if (unprefixedValue != null) {
searchParams.set(prefixedKey, unprefixedValue)
searchParams.delete(key)
setSearchParams(searchParams)
}
const rawValue = React.useMemo<T>(() => { const rawValue = React.useMemo<T>(() => {
const maybeValue = searchParams.get(key) const maybeValue = searchParams.get(prefixedKey)
const defaultValueFrom = lazyDefaultValueInitializer() const defaultValueFrom = lazyDefaultValueInitializer()
return maybeValue != null return maybeValue != null
? safeJsonParse.safeJsonParse(maybeValue, defaultValueFrom, (unknown): unknown is T => true) ? safeJsonParse.safeJsonParse(maybeValue, defaultValueFrom, (unknown): unknown is T => true)
: defaultValueFrom : defaultValueFrom
}, [key, lazyDefaultValueInitializer, searchParams]) }, [prefixedKey, lazyDefaultValueInitializer, searchParams])
const isValueValid = predicateEventCallback(rawValue) const isValueValid = predicateEventCallback(rawValue)
@ -71,7 +82,7 @@ export function useSearchParamsState<T = unknown>(
if (nextValue === lazyDefaultValueInitializer()) { if (nextValue === lazyDefaultValueInitializer()) {
clear() clear()
} else { } else {
searchParams.set(key, JSON.stringify(nextValue)) searchParams.set(prefixedKey, JSON.stringify(nextValue))
setSearchParams(searchParams) setSearchParams(searchParams)
} }
}) })

View File

@ -1,6 +1,8 @@
/** @file The container that launches the IDE. */ /** @file The container that launches the IDE. */
import * as React from 'react' import * as React from 'react'
import * as appUtils from '#/appUtils'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks' import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendModule from '#/services/Backend' import * as backendModule from '#/services/Backend'
@ -114,7 +116,10 @@ export default function Editor(props: EditorProps) {
}, },
}, },
accessToken, accessToken,
{ projectId: project.projectId } {
projectId: project.projectId,
ignoreParamsRegex: new RegExp(`^${appUtils.SEARCH_PARAMS_PREFIX}(.+)$`),
}
) )
} catch (error) { } catch (error) {
toastAndLog('openEditorError', error) toastAndLog('openEditorError', error)