From 672d948d24b9bae8de2dcd1759ffa3e3f7e53054 Mon Sep 17 00:00:00 2001 From: andreas-unleash <104830839+andreas-unleash@users.noreply.github.com> Date: Tue, 12 Jul 2022 18:13:18 +0300 Subject: [PATCH] Playground results light --- .../PlaygroundConnectionFieldset.tsx | 2 +- .../ContextBanner/ContextBanner.tsx | 26 ++ .../FeatureStatusCell/FeatureStatusCell.tsx | 70 +++++ .../PlaygroundResultsTable.tsx | 238 +++++++++++++++++ .../playground/Playground/playground.model.ts | 248 ++++++++++++++++++ 5 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx create mode 100644 frontend/src/component/playground/Playground/playground.model.ts diff --git a/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx index 43ed6b3ba0..84ec89e74b 100644 --- a/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, useMemo, useState, VFC } from 'react'; +import { ComponentProps, useState, VFC } from 'react'; import { Autocomplete, Box, diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx new file mode 100644 index 0000000000..0f282ee654 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/ContextBanner/ContextBanner.tsx @@ -0,0 +1,26 @@ +import { colors } from 'themes/colors'; +import { Alert, styled } from '@mui/material'; +import { SdkContextSchema } from '../../playground.model'; + +interface IContextBannerProps { + context: SdkContextSchema; +} + +const StyledContextFieldList = styled('ul')(() => ({ + color: colors.black, + listStyleType: 'none', + paddingInlineStart: 16, +})); + +export const ContextBanner = ({ context }: IContextBannerProps) => { + return ( + + Your results are generated based on this configuration + + {Object.entries(context).map(([key, value]) => ( +
  • {`${key}: ${value}`}
  • + ))} +
    +
    + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx new file mode 100644 index 0000000000..f46c8c7447 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureStatusCell/FeatureStatusCell.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +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 { Chip, styled, useTheme } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +interface IFeatureStatusCellProps { + enabled: boolean; +} + +const StyledFalseChip = styled(Chip)(() => ({ + width: 80, + borderRadius: '5px', + border: `1px solid ${colors.red['700']}`, + backgroundColor: colors.red['200'], + ['& .MuiChip-label']: { + color: colors.red['700'], + }, + ['& .MuiChip-icon']: { + color: colors.red['700'], + }, +})); + +const StyledTrueChip = styled(Chip)(() => ({ + width: 80, + borderRadius: '5px', + border: `1px solid ${colors.green['700']}`, + backgroundColor: colors.green['100'], + ['& .MuiChip-label']: { + color: colors.green['700'], + }, + ['& .MuiChip-icon']: { + color: colors.green['700'], + }, +})); + +export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => { + const theme = useTheme(); + const icon = ( + + } + elseShow={ + + } + /> + ); + + const label = enabled ? 'True' : 'False'; + + return ( + + } + elseShow={} + /> + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx new file mode 100644 index 0000000000..3688c92858 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx @@ -0,0 +1,238 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { SortingRule, useGlobalFilter, useSortBy, useTable } from 'react-table'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import { + SortableTableHeader, + Table, + TableBody, + TableCell, + TablePlaceholder, + TableRow, +} 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 } from '../playground.model'; + +const defaultSort: SortingRule = { id: 'name' }; +const { value, setValue } = createLocalStorage( + 'PlaygroundResultsTable:v1', + defaultSort +); + +interface IPlaygroundResultsTableProps { + features?: PlaygroundFeatureSchema[]; + loading: boolean; +} + +export const PlaygroundResultsTable = ({ + features, + loading, +}: IPlaygroundResultsTableProps) => { + const [searchParams, setSearchParams] = useSearchParams(); + + const [searchValue, setSearchValue] = useState( + searchParams.get('search') || '' + ); + + const { + data: searchedData, + getSearchText, + getSearchContext, + } = useSearch(COLUMNS, searchValue, features || []); + + const data = useMemo(() => { + return loading + ? Array(5).fill({ + name: 'Feature name', + project: 'Feature Project', + variant: 'Feature variant', + enabled: 'Feature state', + }) + : searchedData; + }, [searchedData, loading]); + + const [initialState] = useState(() => ({ + sortBy: [ + { + id: searchParams.get('sort') || value.id, + desc: searchParams.has('order') + ? searchParams.get('order') === 'desc' + : value.desc, + }, + ], + })); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + state: { sortBy }, + rows, + prepareRow, + } = useTable( + { + initialState, + columns: COLUMNS as any, + data: data as any, + sortTypes, + autoResetGlobalFilter: false, + autoResetSortBy: false, + disableSortRemove: true, + defaultColumn: { + Cell: HighlightCell, + }, + }, + useGlobalFilter, + useSortBy + ); + + 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; + } + + 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 ( + + } + /> + } + isLoading={loading} + > + ( + + None of the feature toggles were evaluated yet. + + )} + elseShow={() => ( + <> + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
    +
    + 0} + show={ + + No feature toggles found matching “ + {searchValue}” + + } + /> + + )} + /> +
    + ); +}; + +const COLUMNS = [ + { + Header: 'Name', + accessor: 'name', + searchable: true, + width: '60%', + Cell: ({ value }: 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: 170, + Cell: ({ value }: any) => , + }, + { + Header: 'isEnabled', + accessor: 'isEnabled', + maxWidth: 170, + Cell: ({ value }: any) => , + sortType: 'boolean', + }, +]; diff --git a/frontend/src/component/playground/Playground/playground.model.ts b/frontend/src/component/playground/Playground/playground.model.ts new file mode 100644 index 0000000000..fdf501124e --- /dev/null +++ b/frontend/src/component/playground/Playground/playground.model.ts @@ -0,0 +1,248 @@ +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; +}