diff --git a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx index 8c4b063bff..64c555d9f6 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx @@ -1,12 +1,12 @@ import { useEffect, useMemo, useState, VFC } from 'react'; import { useGroups } from 'hooks/api/getters/useGroups/useGroups'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { IGroup } from 'interfaces/group'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Search } from 'component/common/Search/Search'; -import { Button, Grid, useMediaQuery } from '@mui/material'; +import { Grid, useMediaQuery } from '@mui/material'; import theme from 'themes/theme'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { TablePlaceholder } from 'component/common/Table'; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx index d7df4b83d7..bead8645b0 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx @@ -1,5 +1,11 @@ import { useState } from 'react'; -import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + SxProps, + Theme, +} from '@mui/material'; import { IConstraint } from 'interfaces/strategy'; import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody'; import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader'; @@ -15,12 +21,14 @@ interface IConstraintAccordionViewProps { constraint: IConstraint; onDelete?: () => void; onEdit?: () => void; + sx?: SxProps; } export const ConstraintAccordionView = ({ constraint, onEdit, onDelete, + sx = undefined, }: IConstraintAccordionViewProps) => { const { classes: styles } = useStyles(); const [expandable, setExpandable] = useState(true); @@ -30,7 +38,6 @@ export const ConstraintAccordionView = ({ [...semVerOperators, ...numOperators, ...dateOperators], constraint.operator ); - const handleClick = () => { if (expandable) { setExpanded(!expanded); @@ -42,6 +49,7 @@ export const ConstraintAccordionView = ({ className={styles.accordion} classes={{ root: styles.accordionRoot }} expanded={expanded} + sx={sx} > ({ }, })); +const StyledHeaderWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + width: '100%', + justifyContent: 'space-between', + borderRadius: theme.spacing(1), +})); + interface ConstraintAccordionViewHeaderMetaInfoProps { constraint: IConstraint; singleValue: boolean; expanded: boolean; allowExpand: (shouldExpand: boolean) => void; + maxLength?: number; } export const ConstraintAccordionViewHeaderInfo = ({ @@ -39,31 +47,37 @@ export const ConstraintAccordionViewHeaderInfo = ({ singleValue, allowExpand, expanded, + maxLength = 112, //The max number of characters in the values text for NOT allowing expansion }: ConstraintAccordionViewHeaderMetaInfoProps) => { const { classes: styles } = useStyles(); + return ( -
- - {constraint.contextName} - - - - } - elseShow={ - - } - /> -
+ +
+ + + {constraint.contextName} + + + + + } + elseShow={ + + } + /> +
+
); }; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue.tsx index 8906c2ca1b..78ac4dae81 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue.tsx @@ -1,9 +1,9 @@ import React, { useEffect } from 'react'; import { Chip, styled } from '@mui/material'; -import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue'; +import { formatConstraintValue } from 'utils/formatConstraintValue'; import { useStyles } from '../../../ConstraintAccordion.styles'; import { IConstraint } from '../../../../../../interfaces/strategy'; -import { useLocationSettings } from '../../../../../../hooks/useLocationSettings'; +import { useLocationSettings } from 'hooks/useLocationSettings'; const StyledSingleValueChip = styled(Chip)(({ theme }) => ({ margin: 'auto 0', diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.styles.ts similarity index 88% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts rename to frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.styles.ts index 4ef86b1b6d..a196ad6052 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts +++ b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.styles.ts @@ -26,6 +26,11 @@ export const useStyles = makeStyles()(theme => ({ actions: { marginLeft: 'auto', display: 'flex', + minHeight: theme.spacing(6), + alignItems: 'center', + }, + resultChip: { + marginLeft: 'auto', }, body: { padding: theme.spacing(2), diff --git a/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx new file mode 100644 index 0000000000..fc3efe8e61 --- /dev/null +++ b/frontend/src/component/common/StrategyItemContainer/StrategyItemContainer.tsx @@ -0,0 +1,96 @@ +import { DragEventHandler, FC, ReactNode } from 'react'; +import { DragIndicator } from '@mui/icons-material'; +import { styled, IconButton, Box } from '@mui/material'; +import classNames from 'classnames'; +import { IFeatureStrategy } from 'interfaces/strategy'; +import { + getFeatureStrategyIcon, + formatStrategyName, +} from 'utils/strategyNames'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './StrategyItemContainer.styles'; + +interface IStrategyItemContainerProps { + strategy: IFeatureStrategy; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; + actions?: ReactNode; + orderNumber?: number; + className?: string; +} + +const DragIcon = styled(IconButton)(({ theme }) => ({ + padding: 0, + cursor: 'inherit', + transition: 'color 0.2s ease-in-out', +})); + +const StyledIndexLabel = styled('div')(({ theme }) => ({ + fontSize: theme.typography.fontSize, + color: theme.palette.text.secondary, + position: 'absolute', + display: 'none', + right: 'calc(100% + 6px)', + top: theme.spacing(2.5), + [theme.breakpoints.up('md')]: { + display: 'block', + }, +})); + +export const StrategyItemContainer: FC = ({ + strategy, + onDragStart, + onDragEnd, + actions, + children, + orderNumber, + className, +}) => { + const { classes: styles } = useStyles(); + const Icon = getFeatureStrategyIcon(strategy.name); + + return ( + + {orderNumber}} + /> + +
+ ( + + + + )} + /> + + +
{actions}
+
+
{children}
+
+
+ ); +}; diff --git a/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx b/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx index 5c44952b07..2ad4f6a8db 100644 --- a/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx +++ b/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx @@ -1,5 +1,5 @@ import { Box } from '@mui/material'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; interface IIconCellProps { icon: ReactNode; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx index efbb434d97..7fab824cb0 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx @@ -1,9 +1,9 @@ -import { Box, styled } from '@mui/material'; +import { DragEventHandler, RefObject, useRef } from 'react'; +import { Box } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { IFeatureStrategy } from 'interfaces/strategy'; -import { DragEventHandler, RefObject, useRef } from 'react'; import { StrategyItem } from './StrategyItem/StrategyItem'; interface IStrategyDraggableItemProps { @@ -22,19 +22,6 @@ interface IStrategyDraggableItemProps { ) => DragEventHandler; onDragEnd: () => void; } - -const StyledIndexLabel = styled('div')(({ theme }) => ({ - fontSize: theme.typography.fontSize, - color: theme.palette.text.secondary, - position: 'absolute', - display: 'none', - right: 'calc(100% + 6px)', - top: theme.spacing(2.5), - [theme.breakpoints.up('md')]: { - display: 'block', - }, -})); - export const StrategyDraggableItem = ({ strategy, index, @@ -58,16 +45,15 @@ export const StrategyDraggableItem = ({ condition={index > 0} show={} /> - - {index + 1} - - + + ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx index 3326954c9b..f47ab515e5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -20,7 +20,6 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator'; interface IStrategyExecutionProps { strategy: IFeatureStrategy; - percentageFill?: string; } const NoItems: VFC = () => ( diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx index bcd9fa88a6..ad3d49e4d5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx @@ -1,24 +1,17 @@ -import { DragEventHandler } from 'react'; -import { DragIndicator, Edit } from '@mui/icons-material'; -import { styled, useTheme, IconButton } from '@mui/material'; +import { DragEventHandler, VFC } from 'react'; +import { Edit } from '@mui/icons-material'; import { Link } from 'react-router-dom'; -import classNames from 'classnames'; import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { IFeatureStrategy } from 'interfaces/strategy'; -import { - getFeatureStrategyIcon, - formatStrategyName, -} from 'utils/strategyNames'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { StrategyExecution } from './StrategyExecution/StrategyExecution'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; -import { useStyles } from './StrategyItem.styles'; +import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; interface IStrategyItemProps { environmentId: string; @@ -26,26 +19,19 @@ interface IStrategyItemProps { onDragStart?: DragEventHandler; onDragEnd?: DragEventHandler; otherEnvironments?: IFeatureEnvironment['name'][]; + orderNumber?: number; } -const DragIcon = styled(IconButton)(({ theme }) => ({ - padding: 0, - cursor: 'inherit', - transition: 'color 0.2s ease-in-out', -})); - -export const StrategyItem = ({ +export const StrategyItem: VFC = ({ environmentId, strategy, onDragStart, onDragEnd, otherEnvironments, -}: IStrategyItemProps) => { + orderNumber, +}) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); - const theme = useTheme(); - const { classes: styles } = useStyles(); - const Icon = getFeatureStrategyIcon(strategy.name); const editStrategyPath = formatEditStrategyPath( projectId, @@ -55,38 +41,13 @@ export const StrategyItem = ({ ); return ( -
-
- ( - - - - )} - /> - - -
+ 0 @@ -115,14 +76,10 @@ export const StrategyItem = ({ strategyId={strategy.id} icon /> -
-
-
- -
-
+ + } + > + + ); }; diff --git a/frontend/src/component/playground/Playground/Playground.tsx b/frontend/src/component/playground/Playground/Playground.tsx index 50c8bb72bd..03001ecb66 100644 --- a/frontend/src/component/playground/Playground/Playground.tsx +++ b/frontend/src/component/playground/Playground/Playground.tsx @@ -8,7 +8,7 @@ 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 'hooks/api/actions/usePlayground/playground.model'; +import { PlaygroundResponseSchema } from 'component/playground/Playground/interfaces/playground.model'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm'; import { @@ -203,6 +203,7 @@ export const Playground: VFC<{}> = () => { } elseShow={} diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx index 00383eb524..e5839ec9d9 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx @@ -1,6 +1,6 @@ import { colors } from 'themes/colors'; import { Alert, styled } from '@mui/material'; -import { SdkContextSchema } from 'hooks/api/actions/usePlayground/playground.model'; +import { SdkContextSchema } from 'component/playground/Playground/interfaces/playground.model'; interface IContextBannerProps { environment: string; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.styles.ts new file mode 100644 index 0000000000..307018fdc8 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.styles.ts @@ -0,0 +1,29 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + titleRowWrapper: { + display: 'flex', + justifyContent: 'space-between', + width: '100%', + }, + titleRow: { + display: 'inline-flex', + alignItems: 'flex-start', + justifyContent: 'center', + gap: theme.spacing(1.5), + marginTop: theme.spacing(1.5), + }, + alertRow: { + margin: theme.spacing(1, 0), + }, + descriptionRow: { + margin: theme.spacing(1, 0.5), + }, + name: { + fontWeight: 600, + padding: theme.spacing(0.5), + }, + icon: { + textAlign: 'right', + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx new file mode 100644 index 0000000000..96a7168ee8 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx @@ -0,0 +1,109 @@ +import { + PlaygroundFeatureSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { Alert, IconButton, Typography, useTheme } from '@mui/material'; +import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip'; +import { useStyles } from './FeatureDetails.styles'; +import { CloseOutlined } from '@mui/icons-material'; +import React from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + checkForEmptyValues, + hasCustomStrategies, + hasOnlyCustomStrategies, +} from './helpers'; + +interface PlaygroundFeatureResultDetailsProps { + feature: PlaygroundFeatureSchema; + input?: PlaygroundRequestSchema; + onClose: () => void; +} +export const FeatureDetails = ({ + feature, + input, + onClose, +}: PlaygroundFeatureResultDetailsProps) => { + const { classes: styles } = useStyles(); + const theme = useTheme(); + + const description = feature.isEnabled + ? `This feature toggle is True in ${input?.environment} because ` + : `This feature toggle is False in ${input?.environment} because `; + + const reason = (() => { + if (feature.isEnabled) return 'at least one strategy is True'; + + if (!feature.isEnabledInCurrentEnvironment) + return 'the environment is disabled'; + + if (hasOnlyCustomStrategies(feature)) + return 'no strategies could be fully evaluated'; + + return 'all strategies are either False or could not be fully evaluated'; + })(); + + const color = feature.isEnabled + ? theme.palette.success.main + : theme.palette.error.main; + + const noValueTxt = checkForEmptyValues(input?.context) + ? 'You did not provide a value for your context field in step 2 of the configuration' + : undefined; + + const customStrategiesTxt = hasCustomStrategies(feature) + ? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked as Unevaluated` + : undefined; + + const onCloseClick = + onClose && + ((event: React.SyntheticEvent) => { + event.stopPropagation(); + onClose(); + }); + + return ( + <> +
+
+ + {feature.name} + + + + +
+ + + +
+
+ + {description} + + + {reason} + +
+ + {noValueTxt} + + } + /> + + {customStrategiesTxt} + + } + /> + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/helpers.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/helpers.ts new file mode 100644 index 0000000000..07d59f5891 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/helpers.ts @@ -0,0 +1,33 @@ +import { PlaygroundFeatureSchema } from 'component/playground/Playground/interfaces/playground.model'; + +export const DEFAULT_STRATEGIES = [ + 'default', + 'applicationHostname', + 'flexibleRollout', + 'gradualRolloutRandom', + 'gradualRolloutSessionId', + 'gradualRolloutUserId', + 'remoteAddress', + 'userWithId', +]; + +export function checkForEmptyValues(object?: Object): boolean { + if (object === undefined) { + return true; + } + return Object.values(object).every(v => + v && typeof v === 'object' ? checkForEmptyValues(v) : v === null + ); +} + +export const hasCustomStrategies = (feature: PlaygroundFeatureSchema) => { + return feature.strategies?.data?.find( + strategy => !DEFAULT_STRATEGIES.includes(strategy.name) + ); +}; + +export const hasOnlyCustomStrategies = (feature: PlaygroundFeatureSchema) => { + return !feature.strategies?.data?.find(strategy => + DEFAULT_STRATEGIES.includes(strategy.name) + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.styles.ts new file mode 100644 index 0000000000..3ad2f16301 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.styles.ts @@ -0,0 +1,14 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + popoverPaper: { + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(6), + width: 728, + maxWidth: '100%', + height: 'auto', + overflowY: 'auto', + backgroundColor: theme.palette.tertiary.light, + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx new file mode 100644 index 0000000000..8051b2d86f --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx @@ -0,0 +1,69 @@ +import { + PlaygroundFeatureSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { IconButton, Popover, styled } from '@mui/material'; +import { InfoOutlined } from '@mui/icons-material'; +import React, { useRef, useState } from 'react'; +import { useStyles } from './FeatureResultInfoPopoverCell.styles'; +import { FeatureDetails } from './FeatureDetails/FeatureDetails'; +import { PlaygroundResultFeatureStrategyList } from './FeatureStrategyList/PlaygroundResultFeatureStrategyList'; + +interface FeatureResultInfoPopoverCellProps { + feature: PlaygroundFeatureSchema; + input?: PlaygroundRequestSchema; +} + +const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({ + alignItems: 'flex-end', + color: theme.palette.tertiary.main, +})); + +export const FeatureResultInfoPopoverCell = ({ + feature, + input, +}: FeatureResultInfoPopoverCellProps) => { + const [open, setOpen] = useState(false); + const { classes: styles } = useStyles(); + const ref = useRef(null); + + const togglePopover = () => { + setOpen(!open); + }; + + if (!feature) { + return null; + } + + return ( + + + + + setOpen(false)} + anchorEl={ref.current} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'center', + horizontal: 'left', + }} + classes={{ paper: styles.popoverPaper }} + > + setOpen(false)} + /> + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx new file mode 100644 index 0000000000..d80dd2a9cb --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx @@ -0,0 +1,39 @@ +import { PlaygroundResultStrategyLists } from './StrategyList/playgroundResultStrategyLists'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + PlaygroundFeatureSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { Alert } from '@mui/material'; + +interface PlaygroundResultFeatureStrategyListProps { + feature: PlaygroundFeatureSchema; + input?: PlaygroundRequestSchema; +} + +export const PlaygroundResultFeatureStrategyList = ({ + feature, + input, +}: PlaygroundResultFeatureStrategyListProps) => { + return ( + <> + + If environment would be enabled then this feature would + be {feature.strategies?.result ? 'TRUE' : 'FALSE'} and + the strategies would evaluate like this:{' '} + + } + /> + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.styles.ts new file mode 100644 index 0000000000..a6721ebb07 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.styles.ts @@ -0,0 +1,40 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + header: { + display: 'flex', + padding: theme.spacing(2, 2), + justifyContent: 'space-between', + }, + headerName: { + padding: theme.spacing(0.5, 2), + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + borderBottom: `1px solid ${theme.palette.divider}`, + fontWeight: theme.typography.fontWeightMedium, + }, + icon: { + fill: theme.palette.inactiveIcon, + }, + resultChip: { + marginLeft: 'auto', + }, + body: { + padding: theme.spacing(2), + justifyItems: 'center', + }, + innerContainer: { + [theme.breakpoints.down(400)]: { + padding: '0.5rem', + }, + width: '100%', + flexShrink: 0, + paddingBottom: '1rem', + borderRadius: theme.shape.borderRadiusMedium, + background: theme.palette.background.default, + }, + successBorder: { + border: `1px solid ${theme.palette.success.main}`, + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx new file mode 100644 index 0000000000..35eccbc480 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx @@ -0,0 +1,62 @@ +import { useTheme } from '@mui/material'; +import { PlaygroundResultChip } from '../../../../PlaygroundResultChip/PlaygroundResultChip'; +import { + PlaygroundStrategySchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { StrategyExecution } from './StrategyExecution/StrategyExecution'; +import { useStyles } from './FeatureStrategyItem.styles'; +import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; +import { objectId } from 'utils/objectId'; + +interface IFeatureStrategyItemProps { + strategy: PlaygroundStrategySchema; + index: number; + input?: PlaygroundRequestSchema; +} + +export const FeatureStrategyItem = ({ + strategy, + input, + index, +}: IFeatureStrategyItemProps) => { + const { result } = strategy; + const { classes: styles } = useStyles(); + const theme = useTheme(); + const label = + result.evaluationStatus === 'incomplete' + ? 'Unevaluated' + : result.enabled + ? 'True' + : 'False'; + + return ( + + } + > + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordion.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordion.styles.ts new file mode 100644 index 0000000000..64e00cc2f9 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordion.styles.ts @@ -0,0 +1,106 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + constraintIconContainer: { + backgroundColor: theme.palette.primary.main, + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: theme.spacing(1), + [theme.breakpoints.down(650)]: { + marginBottom: '1rem', + marginRight: 0, + }, + }, + constraintIcon: { + fill: '#fff', + }, + accordion: { + border: `1px solid ${theme.palette.dividerAlternative}`, + borderRadius: theme.spacing(1), + backgroundColor: '#fff', + boxShadow: 'none', + margin: 0, + }, + accordionRoot: { + '&:before': { + opacity: '0 !important', + }, + }, + headerMetaInfo: { + display: 'flex', + alignItems: 'stretch', + [theme.breakpoints.down(710)]: { flexDirection: 'column' }, + }, + headerContainer: { + display: 'flex', + alignItems: 'center', + [theme.breakpoints.down(710)]: { + flexDirection: 'column', + alignItems: 'center', + position: 'relative', + }, + }, + headerValuesContainerWrapper: { + display: 'flex', + alignItems: 'stretch', + margin: 'auto 0', + }, + headerValuesContainer: { + display: 'flex', + justifyContent: 'stretch', + margin: 'auto 0', + flexDirection: 'column', + }, + headerValues: { + fontSize: theme.fontSizes.smallBody, + }, + headerValuesExpand: { + fontSize: theme.fontSizes.smallBody, + marginTop: '4px', + color: theme.palette.primary.dark, + [theme.breakpoints.down(710)]: { + textAlign: 'center', + }, + }, + headerConstraintContainer: { + minWidth: '220px', + position: 'relative', + paddingRight: '1rem', + [theme.breakpoints.between(1101, 1365)]: { + minWidth: '152px', + paddingRight: '0.5rem', + }, + }, + headerText: { + maxWidth: '400px', + fontSize: theme.fontSizes.smallBody, + [theme.breakpoints.down('xl')]: { + display: 'none', + }, + }, + chip: { + margin: '0 0.5rem 0.5rem 0', + }, + chipValue: { + whiteSpace: 'pre', + }, + accordionDetails: { + borderTop: `1px dashed ${theme.palette.grey[300]}`, + display: 'flex', + flexDirection: 'column', + }, + valuesContainer: { + padding: '1rem 0rem', + maxHeight: '400px', + overflowY: 'auto', + }, + summary: { + border: 'none', + padding: theme.spacing(0.5, 3), + '&:hover .valuesExpandLabel': { + textDecoration: 'underline', + }, + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx new file mode 100644 index 0000000000..c55a819b92 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx @@ -0,0 +1,91 @@ +import { useState, VFC } from 'react'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + SxProps, + Theme, + useTheme, +} from '@mui/material'; +import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader'; +import { oneOf } from 'utils/oneOf'; +import { + dateOperators, + numOperators, + semVerOperators, +} from 'constants/operators'; +import { useStyles } from './ConstraintAccordion.styles'; +import { + PlaygroundConstraintSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { ConstraintAccordionViewBody } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody'; + +interface IConstraintAccordionViewProps { + constraint: PlaygroundConstraintSchema; + playgroundInput?: PlaygroundRequestSchema; + maxLength?: number; + sx?: SxProps; +} + +export const ConstraintAccordionView: VFC = ({ + constraint, + sx = undefined, + maxLength, + playgroundInput, +}) => { + const { classes: styles } = useStyles(); + const [expandable, setExpandable] = useState(true); + const [expanded, setExpanded] = useState(false); + const theme = useTheme(); + + const singleValue = oneOf( + [...semVerOperators, ...numOperators, ...dateOperators], + constraint.operator + ); + const handleClick = () => { + if (expandable) { + setExpanded(!expanded); + } + }; + const backgroundColor = Boolean(playgroundInput) + ? !Boolean((constraint as PlaygroundConstraintSchema).result) + ? theme.palette.neutral.light + : 'inherit' + : 'inherit'; + + return ( + + + + + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx new file mode 100644 index 0000000000..ee3d96c529 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx @@ -0,0 +1,42 @@ +import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon'; +import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo'; +import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; +import { + PlaygroundConstraintSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; + +interface PlaygroundConstraintAccordionViewHeaderProps { + constraint: PlaygroundConstraintSchema; + singleValue: boolean; + expanded: boolean; + allowExpand: (shouldExpand: boolean) => void; + playgroundInput?: PlaygroundRequestSchema; + maxLength?: number; +} + +export const ConstraintAccordionViewHeader = ({ + constraint, + singleValue, + allowExpand, + expanded, + maxLength, + playgroundInput, +}: PlaygroundConstraintAccordionViewHeaderProps) => { + const { classes: styles } = useStyles(); + + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx new file mode 100644 index 0000000000..00aacc463a --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx @@ -0,0 +1,111 @@ +import { styled, Tooltip, Typography, useTheme } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { PlaygroundSingleValue } from './PlaygroundSingleValue/PlaygroundSingleValue'; +import { PLaygroundMultipleValues } from './PlaygroundMultipleValues/PLaygroundMultipleValues'; +import React from 'react'; +import { useStyles } from '../../ConstraintAccordion.styles'; +import { CancelOutlined } from '@mui/icons-material'; +import { + PlaygroundConstraintSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { ConstraintViewHeaderOperator } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator'; + +const StyledHeaderText = styled('span')(({ theme }) => ({ + display: '-webkit-box', + WebkitLineClamp: 3, + WebkitBoxOrient: 'vertical', + overflow: 'hidden', + maxWidth: '100px', + minWidth: '100px', + marginRight: '10px', + marginTop: 'auto', + marginBottom: 'auto', + wordBreak: 'break-word', + fontSize: theme.fontSizes.smallBody, + [theme.breakpoints.down(710)]: { + textAlign: 'center', + padding: theme.spacing(1, 0), + marginRight: 'inherit', + maxWidth: 'inherit', + }, +})); + +const StyledHeaderWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + width: '100%', + justifyContent: 'space-between', + borderRadius: theme.spacing(1), +})); + +interface PlaygroundConstraintAccordionViewHeaderInfoProps { + constraint: PlaygroundConstraintSchema; + singleValue: boolean; + expanded: boolean; + allowExpand: (shouldExpand: boolean) => void; + result?: boolean; + maxLength?: number; + playgroundInput?: PlaygroundRequestSchema; +} + +export const ConstraintAccordionViewHeaderInfo = ({ + constraint, + singleValue, + allowExpand, + expanded, + result, + playgroundInput, + maxLength = 112, +}: PlaygroundConstraintAccordionViewHeaderInfoProps) => { + const { classes: styles } = useStyles(); + const theme = useTheme(); + + const constraintExistsInContext = Boolean( + playgroundInput?.context[constraint.contextName] + ); + + return ( + +
+ + + {constraint.contextName} + + {playgroundInput?.context[constraint.contextName] || + 'no value'} + + + + + + } + elseShow={ + + } + /> +
+ } + /> +
+ ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundMultipleValues/PLaygroundMultipleValues.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundMultipleValues/PLaygroundMultipleValues.tsx new file mode 100644 index 0000000000..9ec547c758 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundMultipleValues/PLaygroundMultipleValues.tsx @@ -0,0 +1,85 @@ +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { styled, Typography } from '@mui/material'; +import React, { useEffect, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import { useStyles } from '../../../ConstraintAccordion.styles'; +import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model'; + +const StyledValuesSpan = styled('span')(({ theme }) => ({ + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + overflow: 'hidden', + wordBreak: 'break-word', + fontSize: theme.fontSizes.smallBody, + margin: 'auto 0', + [theme.breakpoints.down(710)]: { + margin: theme.spacing(1, 0), + textAlign: 'center', + }, +})); + +interface PLaygroundConstraintAccordionViewHeaderMultipleValueProps { + constraint: PlaygroundConstraintSchema; + expanded: boolean; + maxLength: number; + allowExpand: (shouldExpand: boolean) => void; +} + +export const PLaygroundMultipleValues = ({ + constraint, + expanded, + allowExpand, + maxLength, +}: PLaygroundConstraintAccordionViewHeaderMultipleValueProps) => { + const { classes: styles } = useStyles(); + + const [expandable, setExpandable] = useState(false); + + const text = useMemo(() => { + return constraint?.values?.map(value => value).join(', '); + }, [constraint]); + + useEffect(() => { + if (text) { + allowExpand((text?.length ?? 0) > maxLength); + setExpandable((text?.length ?? 0) > maxLength); + } + }, [text, maxLength, allowExpand, setExpandable]); + + return ( +
+
+ + does not match any values{' '} + + } + /> + {text} + + {!expanded + ? `View all (${constraint?.values?.length})` + : 'View less'} +

+ } + /> +
+
+ ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundSingleValue/PlaygroundSingleValue.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundSingleValue/PlaygroundSingleValue.tsx new file mode 100644 index 0000000000..7c1dabdd6e --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/PlaygroundSingleValue/PlaygroundSingleValue.tsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react'; +import { Chip, styled, Typography } from '@mui/material'; +import { formatConstraintValue } from 'utils/formatConstraintValue'; +import { useStyles } from '../../../ConstraintAccordion.styles'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +const StyledSingleValueChip = styled(Chip)(({ theme }) => ({ + margin: 'auto 0', + [theme.breakpoints.down(710)]: { + margin: theme.spacing(1, 0), + }, +})); + +interface PlaygroundConstraintAccordionViewHeaderSingleValueProps { + constraint: PlaygroundConstraintSchema; + allowExpand: (shouldExpand: boolean) => void; +} + +export const PlaygroundSingleValue = ({ + constraint, + allowExpand, +}: PlaygroundConstraintAccordionViewHeaderSingleValueProps) => { + const { locationSettings } = useLocationSettings(); + const { classes: styles } = useStyles(); + + useEffect(() => { + allowExpand(false); + }, [allowExpand]); + + return ( +
+ + does not match any values{' '} + + } + /> + +
+ ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecution.tsx new file mode 100644 index 0000000000..5b74d5af3d --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecution.tsx @@ -0,0 +1,51 @@ +import { Fragment, VFC } from 'react'; +import { + PlaygroundConstraintSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { objectId } from 'utils/objectId'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { styled } from '@mui/material'; +import { ConstraintAccordionView } from './ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; + +interface IConstraintExecutionProps { + constraints?: PlaygroundConstraintSchema[]; + compact: boolean; + input?: PlaygroundRequestSchema; +} + +export const ConstraintExecutionWrapper = styled('div')(() => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', +})); + +export const ConstraintExecution: VFC = ({ + constraints, + compact, + input, +}) => { + if (!constraints) return null; + + return ( + + {constraints?.map((constraint, index) => ( + + 0 && constraints?.length > 1} + show={} + /> + + + ))} + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/CustomStrategyParams/CustomStrategyParams.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/CustomStrategyParams/CustomStrategyParams.tsx new file mode 100644 index 0000000000..fcfcf2d568 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/CustomStrategyParams/CustomStrategyParams.tsx @@ -0,0 +1,140 @@ +import React, { Fragment, VFC } from 'react'; +import { + parseParameterNumber, + parseParameterString, + parseParameterStrings, +} from 'utils/parseParameter'; +import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { Chip } from '@mui/material'; +import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; +import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model'; +import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; + +interface ICustomStrategyProps { + parameters: { [key: string]: string }; + strategyName: string; + constraints: PlaygroundConstraintSchema[]; +} + +export const CustomStrategyParams: VFC = ({ + strategyName, + constraints, + parameters, +}) => { + const { strategies } = useStrategies(); + const definition = strategies.find(strategyDefinition => { + return strategyDefinition.name === strategyName; + }); + + if (!definition?.editable) { + return null; + } + + const renderCustomStrategyParameters = () => { + return definition?.parameters.map((param: any, index: number) => { + const notLastItem = index !== definition?.parameters?.length - 1; + switch (param?.type) { + case 'list': + const values = parseParameterStrings( + parameters[param.name] + ); + return ( + + + } + /> + + ); + case 'percentage': + return ( + +
+ {' '} + of your base{' '} + {constraints?.length > 0 + ? 'who match constraints' + : ''}{' '} + is included. +
+ + } + /> +
+ ); + case 'boolean': + const bool = Boolean(parameters[param?.name]); + return ( + + + } + /> + + ); + case 'string': + const value = + parseParameterString(parameters[param.name]) ?? + 'no value'; + return ( + + + } + /> + + ); + case 'number': + const number = parseParameterNumber(parameters[param.name]); + return ( + + + } + /> + + ); + case 'default': + return null; + } + return null; + }); + }; + + return <>{renderCustomStrategyParameters()}; +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx new file mode 100644 index 0000000000..0653054755 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx @@ -0,0 +1,83 @@ +import { Chip, Typography, useTheme } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './PlaygroundParametertem.styles'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { CancelOutlined } from '@mui/icons-material'; +import classnames from 'classnames'; + +interface IConstraintItemProps { + value: Array; + text: string; + input?: string | number | boolean | 'no value'; + showReason?: boolean; +} + +export const PlaygroundParameterItem = ({ + value, + text, + input, + showReason = false, +}: IConstraintItemProps) => { + const { classes: styles } = useStyles(); + const theme = useTheme(); + + const color = input === 'no value' ? 'error' : 'neutral'; + const reason = `value does not match any ${text}`; + + return ( +
+ + {`${input}`} + +
+ + {reason} + + } + /> + No {text}s added yet.

} + elseShow={ +
+

+ {value.length}{' '} + {value.length > 1 ? `${text}s` : text} will get + access. +

+ {value.map((v: string | number) => ( + + } + className={styles.chip} + /> + ))} +
+ } + /> +
+ } + elseShow={
} + /> +
+ ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParametertem.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParametertem.styles.ts new file mode 100644 index 0000000000..66b3e69b6f --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParametertem.styles.ts @@ -0,0 +1,32 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + container: { + width: '100%', + padding: theme.spacing(2, 3), + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.spacing(2), + }, + disabled: { + backgroundColor: theme.palette.neutral.light, + opacity: '90%', + }, + chip: { + margin: '0.25rem', + }, + column: { + flexDirection: 'column', + }, + paragraph: { + display: 'inline', + margin: '0.25rem 0', + maxWidth: '95%', + textAlign: 'center', + wordBreak: 'break-word', + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.styles.ts new file mode 100644 index 0000000000..10b1dc7733 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.styles.ts @@ -0,0 +1,12 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + container: {}, + link: { + textDecoration: 'none', + marginLeft: theme.spacing(1), + '&:hover': { + textDecoration: 'underline', + }, + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.tsx new file mode 100644 index 0000000000..50dfd47536 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecution.tsx @@ -0,0 +1,119 @@ +import { VFC } from 'react'; +import { + PlaygroundSegmentSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution'; +import { CancelOutlined, DonutLarge } from '@mui/icons-material'; +import { Link } from 'react-router-dom'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { useStyles } from './SegmentExecution.styles'; +import { styled, Typography } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +interface ISegmentExecutionProps { + segments?: PlaygroundSegmentSchema[]; + input?: PlaygroundRequestSchema; + hasConstraints: boolean; +} + +const SegmentExecutionLinkWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(2, 3), + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + fontSize: theme.fontSizes.smallBody, + position: 'relative', +})); + +const SegmentExecutionHeader = styled('div')(({ theme }) => ({ + width: '100%', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'space-between', + '& + &': { + margin: theme.spacing(2), + }, +})); + +const SegmentExecutionWrapper = styled('div')(({ theme }) => ({ + flexDirection: 'column', + borderRadius: theme.shape.borderRadiusMedium, + border: `1px solid ${theme.palette.dividerAlternative}`, + '& + &': { + marginTop: theme.spacing(2), + }, + background: theme.palette.neutral.light, + marginBottom: theme.spacing(1), +})); + +const SegmentExecutionConstraintWrapper = styled('div')(() => ({ + padding: '12px', +})); + +const SegmentResultTextWrapper = styled('div')(({ theme }) => ({ + color: theme.palette.error.main, + display: 'inline-flex', + justifyContent: 'center', + marginRight: '12px', + gap: theme.spacing(1), +})); + +export const SegmentExecution: VFC = ({ + segments, + input, + hasConstraints, +}) => { + const { classes: styles } = useStyles(); + + if (!segments) return null; + return ( + <> + {segments.map((segment, index) => ( + + + + {' '} + Segment:{' '} + + {segment.name} + + + + + segment is false + + + + + + } + /> + + + + + } + /> + + ))} + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.styles.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.styles.ts new file mode 100644 index 0000000000..0e2b46fcd9 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.styles.ts @@ -0,0 +1,19 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + valueContainer: { + display: 'flex', + alignItems: 'center', + gap: '1ch', + }, + valueSeparator: { + color: theme.palette.grey[700], + }, + summary: { + width: 'auto', + height: 'auto', + padding: theme.spacing(2, 3), + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, +})); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx new file mode 100644 index 0000000000..c167572aa5 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -0,0 +1,112 @@ +import { VFC } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { Box, Chip, styled } from '@mui/material'; +import { useStyles } from './StrategyExecution.styles'; +import { + PlaygroundRequestSchema, + PlaygroundStrategySchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution'; +import { SegmentExecution } from './SegmentExecution/SegmentExecution'; +import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters'; +import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParams'; + +interface IStrategyExecutionProps { + strategyResult: PlaygroundStrategySchema; + percentageFill?: string; + input?: PlaygroundRequestSchema; +} + +const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(0), +})); + +const StyledParamWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(0, 0), +})); + +export const StrategyExecution: VFC = ({ + strategyResult, + input, +}) => { + const { name, constraints, segments, parameters } = strategyResult; + + const { uiConfig } = useUiConfig(); + const { classes: styles } = useStyles(); + + const hasConstraints = Boolean(constraints && constraints?.length > 0); + const hasParameters = Object.keys(parameters).length === 0; + + if (!parameters) { + return null; + } + + return ( + + 0) + } + show={ + + } + /> + 0)} + show={ + <> + + 0 && + !hasParameters + )} + show={} + /> + + } + /> + + The standard strategyResult is{' '} + {' '} + for all users. + + } + /> + + + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx new file mode 100644 index 0000000000..2a283cff32 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx @@ -0,0 +1,126 @@ +import { + parseParameterNumber, + parseParameterStrings, +} from 'utils/parseParameter'; +import { Box, Chip } from '@mui/material'; +import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; +import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem'; +import React from 'react'; +import { useStyles } from '../StrategyExecution.styles'; +import { + PlaygroundConstraintSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { getMappedParam } from '../helpers'; + +export interface PlaygroundResultStrategyExecutionParametersProps { + parameters: { [key: string]: string }; + constraints: PlaygroundConstraintSchema[]; + input?: PlaygroundRequestSchema; +} + +export const PlaygroundResultStrategyExecutionParameters = ({ + parameters, + constraints, + input, +}: PlaygroundResultStrategyExecutionParametersProps) => { + const { classes: styles } = useStyles(); + const renderParameters = () => { + return Object.keys(parameters).map(key => { + switch (key) { + case 'rollout': + case 'Rollout': + const percentage = parseParameterNumber(parameters[key]); + return ( + + + + +
+ {' '} + of your base{' '} + {constraints.length > 0 + ? 'who match constraints' + : ''}{' '} + is included. +
+
+ ); + case 'userIds': + case 'UserIds': + const users = parseParameterStrings(parameters[key]); + return ( + + ); + case 'hostNames': + case 'HostNames': + const hosts = parseParameterStrings(parameters[key]); + return ( + + ); + case 'IPs': + const IPs = parseParameterStrings(parameters[key]); + return ( + + ); + case 'stickiness': + case 'groupId': + return null; + default: + return null; + } + }); + }; + + return <>{renderParameters()}; +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/helpers.ts b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/helpers.ts new file mode 100644 index 0000000000..5b4a8c34b5 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/helpers.ts @@ -0,0 +1,10 @@ +export const getMappedParam = (key: string) => { + switch (key.toUpperCase()) { + case 'USERIDS': + return 'userId'; + case 'IPS': + return 'remoteAddress'; + default: + return key; + } +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx new file mode 100644 index 0000000000..e260116ba4 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx @@ -0,0 +1,92 @@ +import { Fragment } from 'react'; +import { Alert, Box, styled, Typography } from '@mui/material'; +import { + PlaygroundFeatureSchema, + PlaygroundStrategySchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; + +const StyledAlertWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + padding: `0, 4px`, + flexDirection: 'column', + borderRadius: theme.shape.borderRadiusMedium, + border: `1px solid ${theme.palette.info.border}`, +})); + +const StyledListWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(1, 0.5), +})); + +const StyledAlert = styled(Alert)(() => ({ + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, +})); + +interface PlaygroundResultStrategyListProps { + strategies: PlaygroundStrategySchema[]; + input?: PlaygroundRequestSchema; +} + +export const PlaygroundResultStrategyLists = ({ + strategies, + input, +}: PlaygroundResultStrategyListProps) => ( + 0} + show={ + <> + {`Strategies (${strategies.length})`} + + {strategies.map((strategy, index) => ( + + 0} + show={} + /> + + + ))} + + + } + /> +); + +interface WrappedPlaygroundResultStrategyListProps + extends PlaygroundResultStrategyListProps { + feature: PlaygroundFeatureSchema; +} + +export const WrappedPlaygroundResultStrategyList = ({ + strategies, + feature, + input, +}: WrappedPlaygroundResultStrategyListProps) => { + return ( + + + If environment would be enabled then this feature would be{' '} + {feature.strategies.result ? 'TRUE' : 'FALSE'} and the + strategies would evaluate like this:{' '} + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx index 9d7f99462c..efffa73f3a 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx @@ -1,40 +1,12 @@ import React from 'react'; -import { colors } from 'themes/colors'; -import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg'; -import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg'; -import { Box, Chip, styled, useTheme } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Box, styled } from '@mui/material'; +import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip'; +import { PlaygroundFeatureSchema } from '../../interfaces/playground.model'; interface IFeatureStatusCellProps { - enabled: boolean; + feature: PlaygroundFeatureSchema; } -const StyledFalseChip = styled(Chip)(({ theme }) => ({ - width: 80, - borderRadius: '5px', - border: `1px solid ${theme.palette.error.main}`, - backgroundColor: colors.red['200'], - ['& .MuiChip-label']: { - color: theme.palette.error.main, - }, - ['& .MuiChip-icon']: { - color: theme.palette.error.main, - }, -})); - -const StyledTrueChip = styled(Chip)(({ theme }) => ({ - width: 80, - borderRadius: '5px', - border: `1px solid ${theme.palette.success.main}`, - backgroundColor: colors.green['100'], - ['& .MuiChip-label']: { - color: theme.palette.success.main, - }, - ['& .MuiChip-icon']: { - color: theme.palette.success.main, - }, -})); - const StyledCellBox = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', @@ -45,35 +17,25 @@ const StyledChipWrapper = styled(Box)(() => ({ marginRight: 'auto', })); -export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => { - const theme = useTheme(); - const icon = ( - - } - elseShow={ - - } - /> - ); - - const label = enabled ? 'True' : 'False'; - +export const FeatureStatusCell = ({ feature }: IFeatureStatusCellProps) => { + const enabled = feature.isEnabled + ? true + : feature.strategies.result === false + ? false + : 'unknown'; + const label = feature.isEnabled + ? 'True' + : feature.strategies.result === false + ? 'False' + : 'Unknown'; return ( - } - elseShow={} + diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx new file mode 100644 index 0000000000..b97f9c90c8 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx @@ -0,0 +1,129 @@ +import { Chip, styled, useTheme } from '@mui/material'; +import { colors } from '../../../../../themes/colors'; +import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender'; +import React from 'react'; +import { ReactComponent as FeatureEnabledIcon } from '../../../../../assets/icons/isenabled-true.svg'; +import { ReactComponent as FeatureDisabledIcon } from '../../../../../assets/icons/isenabled-false.svg'; +import { WarningOutlined } from '@mui/icons-material'; + +interface IResultChipProps { + enabled: boolean | 'unevaluated' | 'unknown'; + label: string; + // Result icon - defaults to true + showIcon?: boolean; + size?: 'default' | 'medium' | 'large'; +} + +export const StyledChip = styled(Chip)<{ width?: number }>( + ({ theme, width }) => ({ + width: width ?? 60, + height: 24, + borderRadius: theme.shape.borderRadius, + fontWeight: theme.typography.fontWeightMedium, + ['& .MuiChip-label']: { + padding: 0, + paddingLeft: theme.spacing(0.5), + }, + }) +); + +export const StyledFalseChip = styled(StyledChip)(({ theme }) => ({ + border: `1px solid ${theme.palette.error.main}`, + backgroundColor: colors.red['200'], + ['& .MuiChip-label']: { + color: theme.palette.error.main, + }, + ['& .MuiChip-icon']: { + color: theme.palette.error.main, + }, +})); + +export const StyledTrueChip = styled(StyledChip)(({ theme }) => ({ + border: `1px solid ${theme.palette.success.main}`, + backgroundColor: colors.green['100'], + ['& .MuiChip-label']: { + color: theme.palette.success.main, + }, + ['& .MuiChip-icon']: { + color: theme.palette.success.main, + }, +})); + +export const StyledUnknownChip = styled(StyledChip)(({ theme }) => ({ + border: `1px solid ${theme.palette.warning.main}`, + backgroundColor: colors.orange['100'], + ['& .MuiChip-label']: { + color: theme.palette.warning.main, + }, + ['& .MuiChip-icon']: { + color: theme.palette.warning.main, + }, +})); + +export const PlaygroundResultChip = ({ + enabled, + label, + showIcon = true, + size = 'default', +}: IResultChipProps) => { + const theme = useTheme(); + const icon = ( + } + elseShow={ + + } + elseShow={ + + } + /> + } + /> + ); + + let chipWidth = 60; + if (size === 'medium') chipWidth = 72; + if (size === 'large') chipWidth = 100; + + return ( + + } + elseShow={ + + } + elseShow={ + + } + /> + } + /> + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx index 2e695ce979..fd5db14d1a 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx @@ -19,10 +19,14 @@ 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 } from 'hooks/api/actions/usePlayground/playground.model'; +import { + PlaygroundFeatureSchema, + PlaygroundRequestSchema, +} from 'component/playground/Playground/interfaces/playground.model'; import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; import useLoading from 'hooks/useLoading'; import { VariantCell } from './VariantCell/VariantCell'; +import { FeatureResultInfoPopoverCell } from './FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell'; const defaultSort: SortingRule = { id: 'name' }; const { value, setValue } = createLocalStorage( @@ -32,11 +36,13 @@ const { value, setValue } = createLocalStorage( interface IPlaygroundResultsTableProps { features?: PlaygroundFeatureSchema[]; + input?: PlaygroundRequestSchema; loading: boolean; } export const PlaygroundResultsTable = ({ features, + input, loading, }: IPlaygroundResultsTableProps) => { const [searchParams, setSearchParams] = useSearchParams(); @@ -48,6 +54,77 @@ export const PlaygroundResultsTable = ({ 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, + width: 200, + Cell: ({ + value, + row: { + original: { variant, feature, variants, isEnabled }, + }, + }: any) => ( + + ), + }, + { + Header: 'isEnabled', + accessor: 'isEnabled', + filterName: 'isEnabled', + filterParsing: (value: boolean) => (value ? 'true' : 'false'), + Cell: ({ row }: any) => ( + + ), + sortType: 'boolean', + sortInverted: true, + }, + { + Header: '', + id: 'info', + Cell: ({ row }: any) => ( + + ), + }, + ]; + }, [input]); + const { data: searchedData, getSearchText, @@ -235,60 +312,3 @@ export const PlaygroundResultsTable = ({ ); }; - -const COLUMNS = [ - { - 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, - width: 200, - Cell: ({ - value, - row: { - original: { variant, feature, variants, isEnabled }, - }, - }: any) => ( - - ), - }, - { - Header: 'isEnabled', - accessor: 'isEnabled', - filterName: 'isEnabled', - filterParsing: (value: boolean) => (value ? 'true' : 'false'), - Cell: ({ value }: any) => , - sortType: 'boolean', - sortInverted: true, - }, -]; diff --git a/frontend/src/component/playground/Playground/interfaces/playground.model.ts b/frontend/src/component/playground/Playground/interfaces/playground.model.ts new file mode 100644 index 0000000000..32bdd40b0e --- /dev/null +++ b/frontend/src/component/playground/Playground/interfaces/playground.model.ts @@ -0,0 +1,321 @@ +/** + * + * 09/08/2022 + * This was copied from the openapi-generator generated files and slightly modified + * because of malformed generation of `anyOf`, `oneOf` + * + * https://github.com/OpenAPITools/openapi-generator/issues/12256 + */ + +import { VariantSchema } from 'openapi'; +import { Operator } from 'constants/operators'; + +export interface PlaygroundConstraintSchema { + /** + * The name of the context field that this constraint should apply to. + * @type {string} + * @memberof PlaygroundConstraintSchema + */ + contextName: string; + /** + * The operator to use when evaluating this constraint. For more information about the various operators, refer to [the strategy constraint operator documentation](https://docs.getunleash.io/advanced/strategy_constraints#strategy-constraint-operators). + * @type {string} + * @memberof PlaygroundConstraintSchema + */ + operator: Operator; + /** + * Whether the operator should be case-sensitive or not. Defaults to `false` (being case-sensitive). + * @type {boolean} + * @memberof PlaygroundConstraintSchema + */ + caseInsensitive?: boolean; + /** + * Whether the result should be negated or not. If `true`, will turn a `true` result into a `false` result and vice versa. + * @type {boolean} + * @memberof PlaygroundConstraintSchema + */ + inverted?: boolean; + /** + * The context values that should be used for constraint evaluation. Use this property instead of `value` for properties that accept multiple values. + * @type {Array} + * @memberof PlaygroundConstraintSchema + */ + values?: Array; + /** + * The context value that should be used for constraint evaluation. Use this property instead of `values` for properties that only accept single values. + * @type {string} + * @memberof PlaygroundConstraintSchema + */ + value?: string; + /** + * Whether this was evaluated as true or false. + * @type {boolean} + * @memberof PlaygroundConstraintSchema + */ + result: boolean; +} + +export interface PlaygroundFeatureSchema { + /** + * The feature's name. + * @type {string} + * @memberof PlaygroundFeatureSchema + */ + name: string; + /** + * The ID of the project that contains this feature. + * @type {string} + * @memberof PlaygroundFeatureSchema + */ + projectId: string; + /** + * The strategies that apply to this feature. + * @type {Array} + * @memberof PlaygroundFeatureSchema + */ + strategies: PlaygroundStrategyResultSchema; + /** + * Whether the feature is active and would be evaluated in the provided environment in a normal SDK context. + * @type {boolean} + * @memberof PlaygroundFeatureSchema + */ + isEnabledInCurrentEnvironment: boolean; + /** + * + * @type {boolean | 'unevaluated'} + * @memberof PlaygroundFeatureSchema + */ + isEnabled: boolean; + /** + * + * @type {PlaygroundFeatureSchemaVariant} + * @memberof PlaygroundFeatureSchema + */ + variant: PlaygroundFeatureSchemaVariant | null; + /** + * + * @type {Array} + * @memberof PlaygroundFeatureSchema + */ + variants: Array; +} + +export interface PlaygroundFeatureSchemaVariant { + /** + * The variant's name. If there is no variant or if the toggle is disabled, this will be `disabled` + * @type {string} + * @memberof PlaygroundFeatureSchemaVariant + */ + name: string; + /** + * Whether the variant is enabled or not. If the feature is disabled or if it doesn't have variants, this property will be `false` + * @type {boolean} + * @memberof PlaygroundFeatureSchemaVariant + */ + enabled: boolean; + /** + * + * @type {PlaygroundFeatureSchemaVariantPayload} + * @memberof PlaygroundFeatureSchemaVariant + */ + payload?: PlaygroundFeatureSchemaVariantPayload; +} + +export interface PlaygroundFeatureSchemaVariantPayload { + /** + * The format of the payload. + * @type {string} + * @memberof PlaygroundFeatureSchemaVariantPayload + */ + type: PlaygroundFeatureSchemaVariantPayloadTypeEnum; + /** + * The payload value stringified. + * @type {string} + * @memberof PlaygroundFeatureSchemaVariantPayload + */ + value: string; +} + +export const playgroundFeatureSchemaVariantPayloadTypeEnum = { + Json: 'json', + Csv: 'csv', + String: 'string', +} as const; +export type PlaygroundFeatureSchemaVariantPayloadTypeEnum = + typeof playgroundFeatureSchemaVariantPayloadTypeEnum[keyof typeof playgroundFeatureSchemaVariantPayloadTypeEnum]; + +export interface PlaygroundRequestSchema { + /** + * The environment to evaluate toggles in. + * @type {string} + * @memberof PlaygroundRequestSchema + */ + environment: string; + /** + * + * @type {PlaygroundRequestSchemaProjects} + * @memberof PlaygroundRequestSchema + */ + projects?: PlaygroundRequestSchemaProjects; + /** + * + * @type {SdkContextSchema} + * @memberof PlaygroundRequestSchema + */ + context: SdkContextSchema; +} + +export type PlaygroundRequestSchemaProjects = Array | string; + +export interface PlaygroundResponseSchema { + /** + * + * @type {PlaygroundRequestSchema} + * @memberof PlaygroundResponseSchema + */ + input: PlaygroundRequestSchema; + /** + * The list of features that have been evaluated. + * @type {Array} + * @memberof PlaygroundResponseSchema + */ + features: Array; +} + +export interface PlaygroundSegmentSchema { + /** + * The segment's id. + * @type {number} + * @memberof PlaygroundSegmentSchema + */ + id: number; + /** + * The name of the segment. + * @type {string} + * @memberof PlaygroundSegmentSchema + */ + name: string; + /** + * Whether this was evaluated as true or false. + * @type {boolean} + * @memberof PlaygroundSegmentSchema + */ + result: boolean; + /** + * The list of constraints in this segment. + * @type {Array} + * @memberof PlaygroundSegmentSchema + */ + constraints: Array; +} + +export interface PlaygroundStrategyResultSchema { + result: boolean | 'unknown'; + data?: Array; +} + +export interface PlaygroundStrategySchema { + /** + * The strategy's name. + * @type {string} + * @memberof PlaygroundStrategySchema + */ + name: string; + /** + * The strategy's id. + * @type {string} + * @memberof PlaygroundStrategySchema + */ + id?: string; + /** + * + * @type {PlaygroundStrategySchemaResult} + * @memberof PlaygroundStrategySchema + */ + result: PlaygroundStrategySchemaResult; + /** + * The strategy's segments and their evaluation results. + * @type {Array} + * @memberof PlaygroundStrategySchema + */ + segments: Array; + /** + * The strategy's constraints and their evaluation results. + * @type {Array} + * @memberof PlaygroundStrategySchema + */ + constraints: Array; + /** + * + * @type {{ [key: string]: string; }} + * @memberof PlaygroundStrategySchema + */ + parameters: { [key: string]: string }; +} + +export enum PlaygroundStrategyResultEvaluationStatusEnum { + complete = 'complete', + incomplete = 'incomplete', +} + +export interface PlaygroundStrategySchemaResult { + /** + * Signals that this strategy was evaluated successfully. + * @type {string} + * @memberof PlaygroundStrategySchemaResult + */ + evaluationStatus?: PlaygroundStrategyResultEvaluationStatusEnum; + /** + * Whether this strategy evaluates to true or not. + * @type {boolean} + * @memberof PlaygroundStrategySchemaResult + */ + enabled: boolean; +} + +export interface SdkContextSchema { + [key: string]: string | any; + /** + * + * @type {string} + * @memberof SdkContextSchema + */ + appName: string; + /** + * + * @type {Date} + * @memberof SdkContextSchema + */ + currentTime?: Date; + /** + * + * @type {string} + * @memberof SdkContextSchema + * @deprecated + */ + environment?: string; + /** + * + * @type {{ [key: string]: string; }} + * @memberof SdkContextSchema + */ + properties?: { [key: string]: string }; + /** + * + * @type {string} + * @memberof SdkContextSchema + */ + remoteAddress?: string; + /** + * + * @type {string} + * @memberof SdkContextSchema + */ + sessionId?: string; + /** + * + * @type {string} + * @memberof SdkContextSchema + */ + userId?: string; +} diff --git a/frontend/src/component/playground/Playground/playground.utils.ts b/frontend/src/component/playground/Playground/playground.utils.ts index f27aa6d483..0012253351 100644 --- a/frontend/src/component/playground/Playground/playground.utils.ts +++ b/frontend/src/component/playground/Playground/playground.utils.ts @@ -1,4 +1,4 @@ -import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model'; +import { PlaygroundResponseSchema } from 'component/playground/Playground/interfaces/playground.model'; import { IEnvironment } from 'interfaces/environments'; export const resolveProjects = ( diff --git a/frontend/src/hooks/api/actions/usePlayground/playground.model.ts b/frontend/src/hooks/api/actions/usePlayground/playground.model.ts deleted file mode 100644 index 3b954902de..0000000000 --- a/frontend/src/hooks/api/actions/usePlayground/playground.model.ts +++ /dev/null @@ -1,250 +0,0 @@ -// TODO: replace with auto-generated openapi code - -export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum { - Json = 'json', - Csv = 'csv', - String = 'string', -} - -export interface PlaygroundFeatureSchemaVariantPayload { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariantPayload - */ - type: PlaygroundFeatureSchemaVariantPayloadTypeEnum; - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariantPayload - */ - value: string; -} - -export interface PlaygroundFeatureSchemaVariant { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariant - */ - name: string; - /** - * - * @type {boolean} - * @memberof PlaygroundFeatureSchemaVariant - */ - enabled: boolean; - /** - * - * @type {PlaygroundFeatureSchemaVariantPayload} - * @memberof PlaygroundFeatureSchemaVariant - */ - payload?: PlaygroundFeatureSchemaVariantPayload; -} - -export interface PlaygroundFeatureSchema { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchema - */ - name: string; - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchema - */ - projectId: string; - /** - * - * @type {boolean} - * @memberof PlaygroundFeatureSchema - */ - isEnabled: boolean; - /** - * - * @type {PlaygroundFeatureSchemaVariant} - * @memberof PlaygroundFeatureSchema - */ - variant: PlaygroundFeatureSchemaVariant | null; -} -export interface PlaygroundResponseSchema { - /** - * - * @type {PlaygroundRequestSchema} - * @memberof PlaygroundResponseSchema - */ - input: PlaygroundRequestSchema; - /** - * - * @type {Array} - * @memberof PlaygroundResponseSchema - */ - features: Array; -} - -export interface PlaygroundRequestSchema { - /** - * - * @type {string} - * @memberof PlaygroundRequestSchema - */ - environment: string; - /** - * - * @type {PlaygroundRequestSchemaProjects} - * @memberof PlaygroundRequestSchema - */ - projects?: Array | string; - /** - * - * @type {SdkContextSchema} - * @memberof PlaygroundRequestSchema - */ - context: SdkContextSchema; -} - -export interface PlaygroundFeatureSchemaVariantPayload { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariantPayload - */ - type: PlaygroundFeatureSchemaVariantPayloadTypeEnum; - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariantPayload - */ - value: string; -} - -export interface PlaygroundFeatureSchemaVariant { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchemaVariant - */ - name: string; - /** - * - * @type {boolean} - * @memberof PlaygroundFeatureSchemaVariant - */ - enabled: boolean; - /** - * - * @type {PlaygroundFeatureSchemaVariantPayload} - * @memberof PlaygroundFeatureSchemaVariant - */ - payload?: PlaygroundFeatureSchemaVariantPayload; -} - -export interface PlaygroundFeatureSchema { - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchema - */ - name: string; - /** - * - * @type {string} - * @memberof PlaygroundFeatureSchema - */ - projectId: string; - /** - * - * @type {boolean} - * @memberof PlaygroundFeatureSchema - */ - isEnabled: boolean; - /** - * - * @type {PlaygroundFeatureSchemaVariant} - * @memberof PlaygroundFeatureSchema - */ - variant: PlaygroundFeatureSchemaVariant | null; -} -export interface PlaygroundResponseSchema { - /** - * - * @type {PlaygroundRequestSchema} - * @memberof PlaygroundResponseSchema - */ - input: PlaygroundRequestSchema; - /** - * - * @type {Array} - * @memberof PlaygroundResponseSchema - */ - features: Array; -} - -export interface PlaygroundRequestSchema { - /** - * - * @type {string} - * @memberof PlaygroundRequestSchema - */ - environment: string; - /** - * - * @type Array | string - * @memberof PlaygroundRequestSchema - */ - projects?: Array | string; - /** - * - * @type {SdkContextSchema} - * @memberof PlaygroundRequestSchema - */ - context: SdkContextSchema; -} - -export interface SdkContextSchema { - [key: string]: string | any; - /** - * - * @type {string} - * @memberof SdkContextSchema - */ - appName: string; - /** - * - * @type {Date} - * @memberof SdkContextSchema - */ - currentTime?: Date; - /** - * - * @type {string} - * @memberof SdkContextSchema - * @deprecated - */ - environment?: string; - /** - * - * @type {{ [key: string]: string; }} - * @memberof SdkContextSchema - */ - properties?: { [key: string]: string }; - /** - * - * @type {string} - * @memberof SdkContextSchema - */ - remoteAddress?: string; - /** - * - * @type {string} - * @memberof SdkContextSchema - */ - sessionId?: string; - /** - * - * @type {string} - * @memberof SdkContextSchema - */ - userId?: string; -} diff --git a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts index fd23b83358..7d2d26a849 100644 --- a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts +++ b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts @@ -2,7 +2,7 @@ import useAPI from '../useApi/useApi'; import { PlaygroundRequestSchema, PlaygroundResponseSchema, -} from './playground.model'; +} from '../../../../component/playground/Playground/interfaces/playground.model'; export const usePlaygroundApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ diff --git a/frontend/src/hooks/useHiddenColumns.ts b/frontend/src/hooks/useHiddenColumns.ts index 29b3bd8b54..f5df5e3bf4 100644 --- a/frontend/src/hooks/useHiddenColumns.ts +++ b/frontend/src/hooks/useHiddenColumns.ts @@ -9,7 +9,7 @@ const useHiddenColumns = ( useEffect(() => { const hidden = condition ? hiddenColumns : []; setHiddenColumns(hidden); - }, [setHiddenColumns, condition]); + }, [setHiddenColumns, hiddenColumns, condition]); }; export default useHiddenColumns;