From 593f83d5d389888a5e62a097f33c6540f2d25754 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 18 Jul 2023 08:49:04 +0200 Subject: [PATCH] feat: advancedPlayground flag used only for runtime control (#4262) --- .../__snapshots__/routes.test.tsx.snap | 9 +- .../playground/Playground/LazyPlayground.tsx | 14 +- .../playground/Playground/Playground.tsx | 223 ------------- .../PlaygroundResultsTable.tsx | 303 ------------------ .../actions/usePlayground/usePlayground.ts | 16 - .../__snapshots__/create-config.test.ts.snap | 2 - src/lib/features/playground/playground.ts | 36 +-- src/lib/types/experimental.ts | 4 - src/server-dev.ts | 1 - 9 files changed, 25 insertions(+), 583 deletions(-) delete mode 100644 frontend/src/component/playground/Playground/Playground.tsx delete mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index 7aac9c6d75..9dde07c560 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -133,7 +133,14 @@ exports[`returns all baseRoutes 1`] = ` "type": "protected", }, { - "component": [Function], + "component": { + "$$typeof": Symbol(react.lazy), + "_init": [Function], + "_payload": { + "_result": [Function], + "_status": -1, + }, + }, "hidden": false, "menu": { "mobile": true, diff --git a/frontend/src/component/playground/Playground/LazyPlayground.tsx b/frontend/src/component/playground/Playground/LazyPlayground.tsx index c27baed30b..9688f1f69e 100644 --- a/frontend/src/component/playground/Playground/LazyPlayground.tsx +++ b/frontend/src/component/playground/Playground/LazyPlayground.tsx @@ -1,15 +1,3 @@ import { lazy } from 'react'; -import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; -export const LazyLegacyPlayground = lazy(() => import('./Playground')); -export const LazyAdvancedPlayground = lazy( - () => import('./AdvancedPlayground') -); - -export const LazyPlayground = () => { - const { uiConfig } = useUiConfig(); - - if (uiConfig.flags.advancedPlayground) return ; - - return ; -}; +export const LazyPlayground = lazy(() => import('./AdvancedPlayground')); diff --git a/frontend/src/component/playground/Playground/Playground.tsx b/frontend/src/component/playground/Playground/Playground.tsx deleted file mode 100644 index f06352799f..0000000000 --- a/frontend/src/component/playground/Playground/Playground.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { FormEventHandler, useEffect, useState, VFC } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { Box, Paper, useMediaQuery, useTheme } from '@mui/material'; -import { PageContent } from 'component/common/PageContent/PageContent'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import useToast from 'hooks/useToast'; -import { formatUnknownError } from 'utils/formatUnknownError'; -import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground'; -import { PlaygroundResponseSchema } from 'openapi'; -import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; -import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm'; -import { - resolveDefaultEnvironment, - resolveProjects, - resolveResultsWidth, -} from './playground.utils'; -import { PlaygroundGuidance } from './PlaygroundGuidance/PlaygroundGuidance'; -import { PlaygroundGuidancePopper } from './PlaygroundGuidancePopper/PlaygroundGuidancePopper'; -import Loader from '../../common/Loader/Loader'; - -export const Playground: VFC<{}> = () => { - const { environments: availableEnvironments } = useEnvironments(); - const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down('lg')); - - const [environments, setEnvironments] = useState([]); - const [projects, setProjects] = useState([]); - const [context, setContext] = useState(); - const [results, setResults] = useState< - PlaygroundResponseSchema | undefined - >(); - const { setToastData } = useToast(); - const [searchParams, setSearchParams] = useSearchParams(); - const { evaluatePlayground, loading } = usePlaygroundApi(); - - useEffect(() => { - setEnvironments([resolveDefaultEnvironment(availableEnvironments)]); - }, [JSON.stringify(availableEnvironments)]); - - useEffect(() => { - // Load initial values from URL - try { - const environmentFromUrl = searchParams.get('environment'); - if (environmentFromUrl) { - setEnvironments([environmentFromUrl]); - } - - let projectsArray: string[]; - let projectsFromUrl = searchParams.get('projects'); - if (projectsFromUrl) { - projectsArray = projectsFromUrl.split(','); - setProjects(projectsArray); - } - - let contextFromUrl = searchParams.get('context'); - if (contextFromUrl) { - contextFromUrl = decodeURI(contextFromUrl); - setContext(contextFromUrl); - } - - const makePlaygroundRequest = async () => { - if (environmentFromUrl && contextFromUrl) { - await evaluatePlaygroundContext( - environmentFromUrl, - projectsArray || '*', - contextFromUrl - ); - } - }; - - makePlaygroundRequest(); - } catch (error) { - setToastData({ - type: 'error', - title: `Failed to parse URL parameters: ${formatUnknownError( - error - )}`, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const evaluatePlaygroundContext = async ( - environment: string, - projects: string[] | string, - context: string | undefined, - action?: () => void - ) => { - try { - const parsedContext = JSON.parse(context || '{}'); - const response = await evaluatePlayground({ - environment, - projects: resolveProjects(projects), - context: { - appName: 'playground', - ...parsedContext, - }, - }); - - if (action && typeof action === 'function') { - action(); - } - setResults(response); - } catch (error: unknown) { - setToastData({ - type: 'error', - title: `Error parsing context: ${formatUnknownError(error)}`, - }); - } - }; - - const onSubmit: FormEventHandler = async event => { - event.preventDefault(); - - await evaluatePlaygroundContext( - environments[0], - projects, - context, - setURLParameters - ); - }; - - const setURLParameters = () => { - searchParams.set('context', encodeURI(context || '')); // always set because of native validation - searchParams.set('environment', environments[0]); - if ( - Array.isArray(projects) && - projects.length > 0 && - !(projects.length === 1 && projects[0] === '*') - ) { - searchParams.set('projects', projects.join(',')); - } else { - searchParams.delete('projects'); - } - setSearchParams(searchParams); - }; - - const formWidth = results && !matches ? '35%' : 'auto'; - const resultsWidth = resolveResultsWidth(matches, results); - - return ( - } - /> - } - disableLoading - bodyClass={'no-padding'} - > - - - - - - - ({ - width: resultsWidth, - transition: 'width 0.4s ease', - padding: theme.spacing(4, 2), - })} - > - } - elseShow={ - - } - elseShow={} - /> - } - /> - - - - ); -}; - -export default Playground; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx deleted file mode 100644 index 00e65821a7..0000000000 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { - SortingRule, - useFlexLayout, - useGlobalFilter, - useSortBy, - useTable, -} from 'react-table'; - -import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; -import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; -import { sortTypes } from 'utils/sortTypes'; -import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Search } from 'component/common/Search/Search'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; -import { useSearch } from 'hooks/useSearch'; -import { createLocalStorage } from 'utils/createLocalStorage'; -import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell'; -import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi'; -import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; -import useLoading from 'hooks/useLoading'; -import { VariantCell } from './VariantCell/VariantCell'; -import { FeatureResultInfoPopoverCell } from './FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell'; -import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; - -const defaultSort: SortingRule = { id: 'name' }; -const { value, setValue } = createLocalStorage( - 'PlaygroundResultsTable:v1', - defaultSort -); - -interface IPlaygroundResultsTableProps { - features?: PlaygroundFeatureSchema[]; - input?: PlaygroundRequestSchema; - loading: boolean; -} - -export const PlaygroundResultsTable = ({ - features, - input, - loading, -}: IPlaygroundResultsTableProps) => { - const [searchParams, setSearchParams] = useSearchParams(); - const ref = useLoading(loading); - const [searchValue, setSearchValue] = useState( - searchParams.get('search') || '' - ); - const theme = useTheme(); - const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - - const COLUMNS = useMemo(() => { - return [ - { - Header: 'Name', - accessor: 'name', - searchable: true, - minWidth: 160, - Cell: ({ value, row: { original } }: any) => ( - - ), - }, - { - Header: 'Project ID', - accessor: 'projectId', - sortType: 'alphanumeric', - filterName: 'projectId', - searchable: true, - maxWidth: 170, - Cell: ({ value }: any) => ( - - ), - }, - { - Header: 'Variant', - id: 'variant', - accessor: 'variant.name', - sortType: 'alphanumeric', - filterName: 'variant', - searchable: true, - maxWidth: 200, - Cell: ({ - value, - row: { - original: { variant, feature, variants, isEnabled }, - }, - }: any) => ( - - ), - }, - { - id: 'isEnabled', - Header: 'isEnabled', - filterName: 'isEnabled', - accessor: (row: PlaygroundFeatureSchema) => - row?.isEnabled - ? 'true' - : row?.strategies?.result === 'unknown' - ? 'unknown' - : 'false', - Cell: ({ row }: any) => ( - - ), - sortType: 'playgroundResultState', - maxWidth: 120, - sortInverted: true, - }, - { - Header: '', - maxWidth: 70, - id: 'info', - Cell: ({ row }: any) => ( - - ), - }, - ]; - }, [input]); - - const { - data: searchedData, - getSearchText, - getSearchContext, - } = useSearch(COLUMNS, searchValue, features || []); - - const data = useMemo(() => { - return loading - ? Array(5).fill({ - name: 'Feature name', - projectId: 'FeatureProject', - variant: { name: 'FeatureVariant', variants: [] }, - enabled: true, - }) - : searchedData; - }, [searchedData, loading]); - - const [initialState] = useState(() => ({ - sortBy: [ - { - id: searchParams.get('sort') || value.id, - desc: searchParams.has('order') - ? searchParams.get('order') === 'desc' - : value.desc, - }, - ], - })); - - const { - headerGroups, - rows, - state: { sortBy }, - prepareRow, - setHiddenColumns, - } = useTable( - { - initialState, - columns: COLUMNS as any, - data: data as any, - sortTypes, - autoResetGlobalFilter: false, - autoResetHiddenColumns: false, - autoResetSortBy: false, - disableSortRemove: true, - disableMultiSort: true, - defaultColumn: { - Cell: HighlightCell, - }, - }, - useGlobalFilter, - useFlexLayout, - useSortBy - ); - - useConditionallyHiddenColumns( - [ - { - condition: isExtraSmallScreen, - columns: ['variant'], - }, - { - condition: isSmallScreen, - columns: ['projectId'], - }, - ], - setHiddenColumns, - COLUMNS - ); - - useEffect(() => { - if (loading) { - return; - } - const tableState: Record = - Object.fromEntries(searchParams); - tableState.sort = sortBy[0].id; - if (sortBy[0].desc) { - tableState.order = 'desc'; - } else if (tableState.order) { - delete tableState.order; - } - if (searchValue) { - tableState.search = searchValue; - } else { - delete tableState.search; - } - - setSearchParams(tableState, { - replace: true, - }); - setValue({ id: sortBy[0].id, desc: sortBy[0].desc || false }); - - // eslint-disable-next-line react-hooks/exhaustive-deps -- don't re-render after search params change - }, [loading, sortBy, searchValue]); - - return ( - <> - - - {features !== undefined && !loading - ? `Results (${ - rows.length < data.length - ? `${rows.length} of ${data.length}` - : data.length - })` - : 'Results'} - - - - - ( - - {data === undefined - ? 'None of the feature toggles were evaluated yet.' - : 'No results found.'} - - )} - elseShow={() => ( - - - - - 0 - } - show={ - - No feature toggles found matching “ - {searchValue}” - - } - /> - - - No features toggles to display - - } - /> - - )} - /> - - ); -}; diff --git a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts index 241abf7104..13ebebdcf8 100644 --- a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts +++ b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts @@ -13,21 +13,6 @@ export const usePlaygroundApi = () => { const URI = 'api/admin/playground'; - const evaluatePlayground = async (payload: PlaygroundRequestSchema) => { - const req = createRequest(URI, { - method: 'POST', - body: JSON.stringify(payload), - }); - - try { - const res = await makeRequest(req.caller, req.id); - - return res.json() as Promise; - } catch (error) { - throw error; - } - }; - const evaluateAdvancedPlayground = async ( payload: AdvancedPlaygroundRequestSchema ) => { @@ -47,7 +32,6 @@ export const usePlaygroundApi = () => { }; return { - evaluatePlayground, evaluateAdvancedPlayground, errors, loading, diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index c9b0e4652d..89a6733d66 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -67,7 +67,6 @@ exports[`should create default config 1`] = ` "isEnabled": [Function], }, "flags": { - "advancedPlayground": false, "anonymiseEventLog": false, "caseInsensitiveInOperators": false, "cleanClientApi": false, @@ -102,7 +101,6 @@ exports[`should create default config 1`] = ` }, "flagResolver": FlagResolver { "experiments": { - "advancedPlayground": false, "anonymiseEventLog": false, "caseInsensitiveInOperators": false, "cleanClientApi": false, diff --git a/src/lib/features/playground/playground.ts b/src/lib/features/playground/playground.ts index a4d3d25996..d21369338b 100644 --- a/src/lib/features/playground/playground.ts +++ b/src/lib/features/playground/playground.ts @@ -55,7 +55,7 @@ export default class PlaygroundController extends Controller { }, requestBody: createRequestSchema('playgroundRequestSchema'), description: - 'Use the provided `context`, `environment`, and `projects` to evaluate toggles on this Unleash instance. Returns a list of all toggles that match the parameters and what they evaluate to. The response also contains the input parameters that were provided.', + 'Deprecated. Will be removed in the next Unleash major update. Use the provided `context`, `environment`, and `projects` to evaluate toggles on this Unleash instance. Returns a list of all toggles that match the parameters and what they evaluate to. The response also contains the input parameters that were provided.', summary: 'Evaluate an Unleash context against a set of environments and projects.', }), @@ -115,27 +115,23 @@ export default class PlaygroundController extends Controller { req: Request, res: Response, ): Promise { - if (this.flagResolver.isEnabled('advancedPlayground')) { - const { payload } = - this.flagResolver.getVariant('advancedPlayground'); - const limit = - payload?.value && Number.isInteger(parseInt(payload?.value)) - ? parseInt(payload?.value) - : 15000; + // used for runtime control, do not remove + const { payload } = this.flagResolver.getVariant('advancedPlayground'); + const limit = + payload?.value && Number.isInteger(parseInt(payload?.value)) + ? parseInt(payload?.value) + : 15000; - const result = await this.playgroundService.evaluateAdvancedQuery( - req.body.projects || '*', - req.body.environments, - req.body.context, - limit, - ); + const result = await this.playgroundService.evaluateAdvancedQuery( + req.body.projects || '*', + req.body.environments, + req.body.context, + limit, + ); - const response: AdvancedPlaygroundResponseSchema = - advancedPlaygroundViewModel(req.body, result); + const response: AdvancedPlaygroundResponseSchema = + advancedPlaygroundViewModel(req.body, result); - res.json(response); - } else { - res.status(409).end(); - } + res.json(response); } } diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index e890fa34e3..3a92a314ed 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -95,10 +95,6 @@ const flags: IFlags = { process.env.DISABLE_NOTIFICATIONS, false, ), - advancedPlayground: parseEnvVarBoolean( - process.env.ADVANCED_PLAYGROUND, - false, - ), customRootRoles: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES, false, diff --git a/src/server-dev.ts b/src/server-dev.ts index 8ad182bbff..c6533fd133 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -37,7 +37,6 @@ process.nextTick(async () => { embedProxyFrontend: true, anonymiseEventLog: false, responseTimeWithAppNameKillSwitch: false, - advancedPlayground: true, strategyVariant: true, newProjectLayout: true, emitPotentiallyStaleEvents: true,