1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Playground results light

This commit is contained in:
andreas-unleash 2022-07-12 18:13:18 +03:00
parent 2e94cd660c
commit 672d948d24
5 changed files with 583 additions and 1 deletions

View File

@ -1,4 +1,4 @@
import { ComponentProps, useMemo, useState, VFC } from 'react';
import { ComponentProps, useState, VFC } from 'react';
import {
Autocomplete,
Box,

View File

@ -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 (
<Alert severity="info" sx={{ my: 2 }}>
Your results are generated based on this configuration
<StyledContextFieldList>
{Object.entries(context).map(([key, value]) => (
<li key={key}>{`${key}: ${value}`}</li>
))}
</StyledContextFieldList>
</Alert>
);
};

View File

@ -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 = (
<ConditionallyRender
condition={enabled}
show={
<FeatureEnabledIcon
stroke={theme.palette.success.main}
strokeWidth="0.25"
/>
}
elseShow={
<FeatureDisabledIcon
stroke={theme.palette.error.main}
strokeWidth="0.25"
/>
}
/>
);
const label = enabled ? 'True' : 'False';
return (
<TextCell>
<ConditionallyRender
condition={enabled}
show={<StyledTrueChip icon={icon} label={label} />}
elseShow={<StyledFalseChip icon={icon} label={label} />}
/>
</TextCell>
);
};

View File

@ -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<string> = { 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<string, string> =
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 (
<PageContent
header={
<PageHeader
titleElement={
features !== undefined
? `Results (${
rows.length < data.length
? `${rows.length} of ${data.length}`
: data.length
})`
: 'Results'
}
actions={
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
}
/>
}
isLoading={loading}
>
<ConditionallyRender
condition={!loading && data.length === 0}
show={() => (
<TablePlaceholder>
None of the feature toggles were evaluated yet.
</TablePlaceholder>
)}
elseShow={() => (
<>
<SearchHighlightProvider
value={getSearchText(searchValue)}
>
<Table {...getTableProps()} rowHeight="standard">
<SortableTableHeader
headerGroups={headerGroups as any}
/>
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps()}
>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider>
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}&rdquo;
</TablePlaceholder>
}
/>
</>
)}
/>
</PageContent>
);
};
const COLUMNS = [
{
Header: 'Name',
accessor: 'name',
searchable: true,
width: '60%',
Cell: ({ value }: any) => (
<LinkCell title={value} to={`/feature/${value}`} />
),
},
{
Header: 'Project ID',
accessor: 'projectId',
sortType: 'alphanumeric',
filterName: 'projectId',
searchable: true,
maxWidth: 170,
Cell: ({ value }: any) => (
<LinkCell title={value} to={`/projects/${value}`} />
),
},
{
Header: 'Variant',
id: 'variant',
accessor: 'variant.name',
sortType: 'alphanumeric',
filterName: 'variant',
searchable: true,
maxWidth: 170,
Cell: ({ value }: any) => <HighlightCell value={value} />,
},
{
Header: 'isEnabled',
accessor: 'isEnabled',
maxWidth: 170,
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
sortType: 'boolean',
},
];

View File

@ -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<PlaygroundFeatureSchema>}
* @memberof PlaygroundResponseSchema
*/
features: Array<PlaygroundFeatureSchema>;
}
export interface PlaygroundRequestSchema {
/**
*
* @type {string}
* @memberof PlaygroundRequestSchema
*/
environment: string;
/**
*
* @type {PlaygroundRequestSchemaProjects}
* @memberof PlaygroundRequestSchema
*/
projects?: Array<string> | 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<PlaygroundFeatureSchema>}
* @memberof PlaygroundResponseSchema
*/
features: Array<PlaygroundFeatureSchema>;
}
export interface PlaygroundRequestSchema {
/**
*
* @type {string}
* @memberof PlaygroundRequestSchema
*/
environment: string;
/**
*
* @type Array<string> | string
* @memberof PlaygroundRequestSchema
*/
projects?: Array<string> | 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;
}