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;
+}