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:
parent
2e94cd660c
commit
672d948d24
@ -1,4 +1,4 @@
|
||||
import { ComponentProps, useMemo, useState, VFC } from 'react';
|
||||
import { ComponentProps, useState, VFC } from 'react';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 “
|
||||
{searchValue}”
|
||||
</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',
|
||||
},
|
||||
];
|
248
frontend/src/component/playground/Playground/playground.model.ts
Normal file
248
frontend/src/component/playground/Playground/playground.model.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user