mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
Merge branch 'main' into fix/playground_virtualisation_loader
This commit is contained in:
commit
1a30f42635
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-frontend",
|
"name": "unleash-frontend",
|
||||||
"description": "unleash your features",
|
"description": "unleash your features",
|
||||||
"version": "4.14.3",
|
"version": "4.15.0-beta.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useMemo, useState, VFC } from 'react';
|
import { useEffect, useMemo, useState, VFC } from 'react';
|
||||||
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
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 { IGroup } from 'interfaces/group';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Search } from 'component/common/Search/Search';
|
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 theme from 'themes/theme';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
import { TablePlaceholder } from 'component/common/Table';
|
||||||
|
@ -13,7 +13,7 @@ import React from 'react';
|
|||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { EDIT } from 'constants/misc';
|
import { EDIT } from 'constants/misc';
|
||||||
import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
interface IUserForm {
|
interface IUserForm {
|
||||||
email: string;
|
email: string;
|
||||||
@ -49,7 +49,7 @@ const UserForm: React.FC<IUserForm> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const { roles } = useUsers();
|
const { roles } = useUsers();
|
||||||
const { bootstrap } = useUiBootstrap();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const sortRoles = (a, b) => {
|
const sortRoles = (a, b) => {
|
||||||
@ -127,7 +127,7 @@ const UserForm: React.FC<IUserForm> = ({
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={mode !== EDIT && bootstrap?.email}
|
condition={mode !== EDIT && Boolean(uiConfig?.emailEnabled)}
|
||||||
show={
|
show={
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography
|
<Typography
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap';
|
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const useCreateUserForm = (
|
const useCreateUserForm = (
|
||||||
initialName = '',
|
initialName = '',
|
||||||
initialEmail = '',
|
initialEmail = '',
|
||||||
initialRootRole = 1
|
initialRootRole = 1
|
||||||
) => {
|
) => {
|
||||||
const { bootstrap } = useUiBootstrap();
|
const { uiConfig } = useUiConfig();
|
||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [email, setEmail] = useState(initialEmail);
|
const [email, setEmail] = useState(initialEmail);
|
||||||
const [sendEmail, setSendEmail] = useState(false);
|
const [sendEmail, setSendEmail] = useState(false);
|
||||||
@ -25,8 +25,8 @@ const useCreateUserForm = (
|
|||||||
}, [initialEmail]);
|
}, [initialEmail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSendEmail(bootstrap?.email || false);
|
setSendEmail(uiConfig?.emailEnabled || false);
|
||||||
}, [bootstrap?.email]);
|
}, [uiConfig?.emailEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRootRole(initialRootRole);
|
setRootRole(initialRootRole);
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip, TooltipProps } from '@mui/material';
|
||||||
import { Info } from '@mui/icons-material';
|
import { Info } from '@mui/icons-material';
|
||||||
import { useStyles } from 'component/common/HelpIcon/HelpIcon.styles';
|
import { useStyles } from 'component/common/HelpIcon/HelpIcon.styles';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IHelpIconProps {
|
interface IHelpIconProps {
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
style?: React.CSSProperties;
|
placement?: TooltipProps['placement'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HelpIcon = ({ tooltip, style }: IHelpIconProps) => {
|
export const HelpIcon = ({ tooltip, placement }: IHelpIconProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltip} arrow>
|
<Tooltip title={tooltip} placement={placement} arrow>
|
||||||
<span
|
<span className={styles.container} tabIndex={0} aria-label="Help">
|
||||||
className={styles.container}
|
|
||||||
style={style}
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Help"
|
|
||||||
>
|
|
||||||
<Info className={styles.icon} />
|
<Info className={styles.icon} />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { IconButton, InputBase, Tooltip } from '@mui/material';
|
import { IconButton, InputBase, Tooltip } from '@mui/material';
|
||||||
import { Search as SearchIcon, Close } from '@mui/icons-material';
|
import { Search as SearchIcon, Close } from '@mui/icons-material';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@ -18,6 +18,7 @@ interface ISearchProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getSearchContext?: () => IGetSearchContextOutput;
|
getSearchContext?: () => IGetSearchContextOutput;
|
||||||
containerStyles?: React.CSSProperties;
|
containerStyles?: React.CSSProperties;
|
||||||
|
debounceTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Search = ({
|
export const Search = ({
|
||||||
@ -29,14 +30,14 @@ export const Search = ({
|
|||||||
disabled,
|
disabled,
|
||||||
getSearchContext,
|
getSearchContext,
|
||||||
containerStyles,
|
containerStyles,
|
||||||
|
debounceTime = 200,
|
||||||
}: ISearchProps) => {
|
}: ISearchProps) => {
|
||||||
const ref = useRef<HTMLInputElement>();
|
const ref = useRef<HTMLInputElement>();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const debouncedOnChange = useAsyncDebounce(onChange, debounceTime);
|
||||||
const debouncedOnChange = useAsyncDebounce(onChange, 200);
|
|
||||||
|
|
||||||
const onSearchChange = (value: string) => {
|
const onSearchChange = (value: string) => {
|
||||||
debouncedOnChange(value);
|
debouncedOnChange(value);
|
||||||
|
130
frontend/src/component/events/EventCard/EventCard.tsx
Normal file
130
frontend/src/component/events/EventCard/EventCard.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { IEvent } from 'interfaces/event';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
interface IEventCardProps {
|
||||||
|
entry: IEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledDefinitionTerm = styled('dt')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledChangesTitle = styled('strong')(({ theme }) => ({
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledContainerListItem = styled('li')(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
gridTemplateColumns: 'auto minmax(0, 1fr)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& dl': {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'auto 1fr',
|
||||||
|
alignContent: 'start',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||||
|
backgroundColor: 'white',
|
||||||
|
overflowX: 'auto',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
borderRadius: 0,
|
||||||
|
borderTopRightRadius: theme.shape.borderRadiusLarge,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
|
},
|
||||||
|
|
||||||
|
'& code': {
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const EventCard = ({ entry }: IEventCardProps) => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
|
||||||
|
const createdAtFormatted = formatDateYMDHMS(
|
||||||
|
entry.createdAt,
|
||||||
|
locationSettings.locale
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainerListItem>
|
||||||
|
<dl>
|
||||||
|
<StyledDefinitionTerm>Event id:</StyledDefinitionTerm>
|
||||||
|
<dd>{entry.id}</dd>
|
||||||
|
<StyledDefinitionTerm>Changed at:</StyledDefinitionTerm>
|
||||||
|
<dd>{createdAtFormatted}</dd>
|
||||||
|
<StyledDefinitionTerm>Event:</StyledDefinitionTerm>
|
||||||
|
<dd>{entry.type}</dd>
|
||||||
|
<StyledDefinitionTerm>Changed by:</StyledDefinitionTerm>
|
||||||
|
<dd title={entry.createdBy}>{entry.createdBy}</dd>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(entry.project)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledDefinitionTerm>
|
||||||
|
Project:
|
||||||
|
</StyledDefinitionTerm>
|
||||||
|
<dd>
|
||||||
|
<Link to={`/projects/${entry.project}`}>
|
||||||
|
{entry.project}
|
||||||
|
</Link>
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(entry.featureName)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledDefinitionTerm>
|
||||||
|
Feature:
|
||||||
|
</StyledDefinitionTerm>
|
||||||
|
<dd>
|
||||||
|
<Link
|
||||||
|
to={`/projects/${entry.project}/features/${entry.featureName}`}
|
||||||
|
>
|
||||||
|
{entry.featureName}
|
||||||
|
</Link>
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={entry.data || entry.preData}
|
||||||
|
show={
|
||||||
|
<StyledCodeSection>
|
||||||
|
<StyledChangesTitle>Changes:</StyledChangesTitle>
|
||||||
|
<EventDiff entry={entry} />
|
||||||
|
</StyledCodeSection>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainerListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventCard;
|
@ -1,8 +1,9 @@
|
|||||||
import { diff } from 'deep-diff';
|
import { diff } from 'deep-diff';
|
||||||
import { useStyles } from './EventDiff.styles';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
import { IEvent } from 'interfaces/event';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
const DIFF_PREFIXES = {
|
const DIFF_PREFIXES: Record<string, string> = {
|
||||||
A: ' ',
|
A: ' ',
|
||||||
E: ' ',
|
E: ' ',
|
||||||
D: '-',
|
D: '-',
|
||||||
@ -14,13 +15,13 @@ interface IEventDiffProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EventDiff = ({ entry }: IEventDiffProps) => {
|
const EventDiff = ({ entry }: IEventDiffProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const theme = useTheme();
|
||||||
|
|
||||||
const KLASSES = {
|
const styles: Record<string, CSSProperties> = {
|
||||||
A: styles.blue, // array edited
|
A: { color: theme.palette.code.edited }, // array edited
|
||||||
E: styles.blue, // edited
|
E: { color: theme.palette.code.edited }, // edited
|
||||||
D: styles.negative, // deleted
|
D: { color: theme.palette.code.diffSub }, // deleted
|
||||||
N: styles.positive, // added
|
N: { color: theme.palette.code.diffAdd }, // added
|
||||||
};
|
};
|
||||||
|
|
||||||
const diffs =
|
const diffs =
|
||||||
@ -32,18 +33,14 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
let change;
|
let change;
|
||||||
if (diff.lhs !== undefined) {
|
if (diff.lhs !== undefined) {
|
||||||
change = (
|
change = (
|
||||||
<div>
|
<div style={styles.D}>
|
||||||
<div className={KLASSES.D}>
|
- {key}: {JSON.stringify(diff.lhs)}
|
||||||
- {key}: {JSON.stringify(diff.lhs)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (diff.rhs !== undefined) {
|
} else if (diff.rhs !== undefined) {
|
||||||
change = (
|
change = (
|
||||||
<div>
|
<div style={styles.N}>
|
||||||
<div className={KLASSES.N}>
|
+ {key}: {JSON.stringify(diff.rhs)}
|
||||||
+ {key}: {JSON.stringify(diff.rhs)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,23 +57,19 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
} else if (diff.lhs !== undefined && diff.rhs !== undefined) {
|
} else if (diff.lhs !== undefined && diff.rhs !== undefined) {
|
||||||
change = (
|
change = (
|
||||||
<div>
|
<div>
|
||||||
<div className={KLASSES.D}>
|
<div style={styles.D}>
|
||||||
- {key}: {JSON.stringify(diff.lhs)}
|
- {key}: {JSON.stringify(diff.lhs)}
|
||||||
</div>
|
</div>
|
||||||
<div className={KLASSES.N}>
|
<div style={styles.N}>
|
||||||
+ {key}: {JSON.stringify(diff.rhs)}
|
+ {key}: {JSON.stringify(diff.rhs)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error
|
|
||||||
const spadenClass = KLASSES[diff.kind];
|
|
||||||
// @ts-expect-error
|
|
||||||
const prefix = DIFF_PREFIXES[diff.kind];
|
|
||||||
|
|
||||||
change = (
|
change = (
|
||||||
<div className={spadenClass}>
|
<div style={styles[diff.kind]}>
|
||||||
{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}
|
{DIFF_PREFIXES[diff.kind]} {key}:{' '}
|
||||||
|
{JSON.stringify(diff.rhs || diff.item)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -91,16 +84,15 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
} else {
|
} else {
|
||||||
// Just show the data if there is no diff yet.
|
// Just show the data if there is no diff yet.
|
||||||
const data = entry.data || entry.preData;
|
const data = entry.data || entry.preData;
|
||||||
changes = (
|
changes = [
|
||||||
<div className={entry.data ? KLASSES.N : KLASSES.D}>
|
<div style={entry.data ? styles.N : styles.D}>
|
||||||
{JSON.stringify(data, null, 2)}
|
{JSON.stringify(data, null, 2)}
|
||||||
</div>
|
</div>,
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }} tabIndex={0}>
|
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }} tabIndex={0}>
|
||||||
{/* @ts-expect-error */}
|
|
||||||
<code>{changes.length === 0 ? '(no changes)' : changes}</code>
|
<code>{changes.length === 0 ? '(no changes)' : changes}</code>
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
@ -1,14 +1,25 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useStyles } from './EventJson.styles';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
import { IEvent } from 'interfaces/event';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
interface IEventJsonProps {
|
interface IEventJsonProps {
|
||||||
entry: IEvent;
|
entry: IEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventJson = ({ entry }: IEventJsonProps) => {
|
export const StyledJsonListItem = styled('li')(({ theme }) => ({
|
||||||
const { classes: styles } = useStyles();
|
padding: theme.spacing(4),
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
|
||||||
|
'& code': {
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
lineHeight: '100%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const EventJson = ({ entry }: IEventJsonProps) => {
|
||||||
const localEventData = JSON.parse(JSON.stringify(entry));
|
const localEventData = JSON.parse(JSON.stringify(entry));
|
||||||
delete localEventData.description;
|
delete localEventData.description;
|
||||||
delete localEventData.name;
|
delete localEventData.name;
|
||||||
@ -17,16 +28,12 @@ const EventJson = ({ entry }: IEventJsonProps) => {
|
|||||||
const prettyPrinted = JSON.stringify(localEventData, null, 2);
|
const prettyPrinted = JSON.stringify(localEventData, null, 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={styles.historyItem}>
|
<StyledJsonListItem>
|
||||||
<div>
|
<div>
|
||||||
<code>{prettyPrinted}</code>
|
<code>{prettyPrinted}</code>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</StyledJsonListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
EventJson.propTypes = {
|
|
||||||
entry: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventJson;
|
export default EventJson;
|
108
frontend/src/component/events/EventLog/EventLog.tsx
Normal file
108
frontend/src/component/events/EventLog/EventLog.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { Switch, FormControlLabel, useMediaQuery, Box } from '@mui/material';
|
||||||
|
import EventJson from 'component/events/EventJson/EventJson';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import EventCard from 'component/events/EventCard/EventCard';
|
||||||
|
import { useEventSettings } from 'hooks/useEventSettings';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useOnVisible } from 'hooks/useOnVisible';
|
||||||
|
import { IEvent } from 'interfaces/event';
|
||||||
|
import { styled } from '@mui/system';
|
||||||
|
|
||||||
|
interface IEventLogProps {
|
||||||
|
title: string;
|
||||||
|
project?: string;
|
||||||
|
feature?: string;
|
||||||
|
displayInline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledEventsList = styled('ul')(({ theme }) => ({
|
||||||
|
listStyleType: 'none',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
display: 'grid',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EventLog = ({
|
||||||
|
title,
|
||||||
|
project,
|
||||||
|
feature,
|
||||||
|
displayInline,
|
||||||
|
}: IEventLogProps) => {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const { events, fetchNextPage } = useEventSearch(project, feature, query);
|
||||||
|
const fetchNextPageRef = useOnVisible<HTMLDivElement>(fetchNextPage);
|
||||||
|
const { eventSettings, setEventSettings } = useEventSettings();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
// Cache the previous search results so that we can show those while
|
||||||
|
// fetching new results for a new search query in the background.
|
||||||
|
const [cache, setCache] = useState<IEvent[]>();
|
||||||
|
useEffect(() => events && setCache(events), [events]);
|
||||||
|
|
||||||
|
const onShowData = () => {
|
||||||
|
setEventSettings(prev => ({ showData: !prev.showData }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchInputField = <Search onChange={setQuery} debounceTime={500} />;
|
||||||
|
|
||||||
|
const showDataSwitch = (
|
||||||
|
<FormControlLabel
|
||||||
|
label="Full events"
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={eventSettings.showData}
|
||||||
|
onChange={onShowData}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
disablePadding={displayInline}
|
||||||
|
disableBorder={displayInline}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title={title}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{showDataSwitch}
|
||||||
|
{!isSmallScreen && searchInputField}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isSmallScreen && searchInputField}
|
||||||
|
</PageHeader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{displayInline && <Box sx={{ mt: 4 }} />}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(cache && cache.length === 0)}
|
||||||
|
show={() => <p>No events found.</p>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(cache && cache.length > 0)}
|
||||||
|
show={() => (
|
||||||
|
<StyledEventsList>
|
||||||
|
{cache?.map(entry => (
|
||||||
|
<ConditionallyRender
|
||||||
|
key={entry.id}
|
||||||
|
condition={eventSettings.showData}
|
||||||
|
show={() => <EventJson entry={entry} />}
|
||||||
|
elseShow={() => <EventCard entry={entry} />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledEventsList>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div ref={fetchNextPageRef} />
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -2,16 +2,16 @@ import React, { useContext } from 'react';
|
|||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { EventHistory } from '../EventHistory/EventHistory';
|
|
||||||
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||||
|
import { EventLog } from 'component/events/EventLog/EventLog';
|
||||||
|
|
||||||
export const EventHistoryPage = () => {
|
export const EventPage = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(ADMIN)}
|
condition={hasAccess(ADMIN)}
|
||||||
show={<EventHistory />}
|
show={() => <EventLog title="Event log" />}
|
||||||
elseShow={<AdminAlert />}
|
elseShow={<AdminAlert />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
@ -1,7 +1,7 @@
|
|||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { useStyles } from './FeatureLog.styles';
|
import { useStyles } from './FeatureLog.styles';
|
||||||
import { FeatureEventHistory } from 'component/history/FeatureEventHistory/FeatureEventHistory';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { EventLog } from 'component/events/EventLog/EventLog';
|
||||||
|
|
||||||
const FeatureLog = () => {
|
const FeatureLog = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
@ -15,7 +15,12 @@ const FeatureLog = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<FeatureEventHistory featureId={feature.name} />
|
<EventLog
|
||||||
|
title="Event log"
|
||||||
|
project={projectId}
|
||||||
|
feature={featureId}
|
||||||
|
displayInline
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
container: {
|
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: '1.5rem',
|
|
||||||
maxWidth: '350px',
|
|
||||||
minWidth: '350px',
|
|
||||||
marginRight: '1rem',
|
|
||||||
marginTop: '1rem',
|
|
||||||
[theme.breakpoints.down(1000)]: {
|
|
||||||
marginBottom: '1rem',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 'none',
|
|
||||||
minWidth: 'auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
fontSize: theme.fontSizes.bodySize,
|
|
||||||
fontWeight: 'normal',
|
|
||||||
margin: 0,
|
|
||||||
marginBottom: '0.5rem',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,14 +1,47 @@
|
|||||||
import { Tooltip } from '@mui/material';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||||
import FeatureOverviewEnvSwitch from './FeatureOverviewEnvSwitch/FeatureOverviewEnvSwitch';
|
import FeatureOverviewEnvSwitch from './FeatureOverviewEnvSwitch/FeatureOverviewEnvSwitch';
|
||||||
import { useStyles } from './FeatureOverviewEnvSwitches.styles';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '1.5rem',
|
||||||
|
maxWidth: '350px',
|
||||||
|
minWidth: '350px',
|
||||||
|
marginRight: '1rem',
|
||||||
|
marginTop: '1rem',
|
||||||
|
[theme.breakpoints.down(1000)]: {
|
||||||
|
marginBottom: '1rem',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
margin: 0,
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
|
||||||
|
// Make the help icon align with the text.
|
||||||
|
'& > :last-child': {
|
||||||
|
position: 'relative',
|
||||||
|
top: 1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const FeatureOverviewEnvSwitches = () => {
|
const FeatureOverviewEnvSwitches = () => {
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { feature } = useFeature(projectId, featureId);
|
||||||
@ -37,15 +70,14 @@ const FeatureOverviewEnvSwitches = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<StyledContainer>
|
||||||
<Tooltip
|
<StyledHeader data-loading>
|
||||||
arrow
|
Feature toggle status
|
||||||
title="Environments can be switched off for a single toggle. Resulting in all calls towards the toggle to return false."
|
<HelpIcon
|
||||||
>
|
tooltip="When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies."
|
||||||
<h3 className={styles.header} data-loading>
|
placement="top"
|
||||||
Feature toggle status
|
/>
|
||||||
</h3>
|
</StyledHeader>
|
||||||
</Tooltip>
|
|
||||||
{renderEnvironmentSwitches()}
|
{renderEnvironmentSwitches()}
|
||||||
<EnvironmentStrategyDialog
|
<EnvironmentStrategyDialog
|
||||||
open={showInfoBox}
|
open={showInfoBox}
|
||||||
@ -54,7 +86,7 @@ const FeatureOverviewEnvSwitches = () => {
|
|||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
environmentName={environmentName}
|
environmentName={environmentName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import EventLog from '../EventLog';
|
|
||||||
import { useEvents } from 'hooks/api/getters/useEvents/useEvents';
|
|
||||||
|
|
||||||
export const EventHistory = () => {
|
|
||||||
const { events } = useEvents();
|
|
||||||
|
|
||||||
if (events.length < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EventLog events={events} title="Event log" />;
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()({
|
|
||||||
eventLogHeader: {
|
|
||||||
minWidth: '110px',
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,57 +0,0 @@
|
|||||||
import EventDiff from 'component/history/EventLog/EventCard/EventDiff/EventDiff';
|
|
||||||
import { useStyles } from './EventCard.styles';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
|
||||||
|
|
||||||
interface IEventCardProps {
|
|
||||||
entry: IEvent;
|
|
||||||
timeFormatted: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EventCard = ({ entry, timeFormatted }: IEventCardProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<dl>
|
|
||||||
<dt className={styles.eventLogHeader}>Event id: </dt>
|
|
||||||
<dd>{entry.id}</dd>
|
|
||||||
<dt className={styles.eventLogHeader}>Changed at:</dt>
|
|
||||||
<dd>{timeFormatted}</dd>
|
|
||||||
<dt className={styles.eventLogHeader}>Event: </dt>
|
|
||||||
<dd>{entry.type}</dd>
|
|
||||||
<dt className={styles.eventLogHeader}>Changed by: </dt>
|
|
||||||
<dd title={entry.createdBy}>{entry.createdBy}</dd>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(entry.project)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<dt className={styles.eventLogHeader}>Project: </dt>
|
|
||||||
<dd>{entry.project}</dd>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(entry.featureName)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<dt className={styles.eventLogHeader}>Feature: </dt>
|
|
||||||
<dd>{entry.featureName}</dd>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</dl>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={entry.data || entry.preData}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<strong>Change</strong>
|
|
||||||
<EventDiff entry={entry} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventCard;
|
|
@ -1,13 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
blue: {
|
|
||||||
color: theme.palette.code.edited,
|
|
||||||
},
|
|
||||||
negative: {
|
|
||||||
color: theme.palette.code.diffSub,
|
|
||||||
},
|
|
||||||
positive: {
|
|
||||||
color: theme.palette.code.diffAdd,
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,10 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
historyItem: {
|
|
||||||
padding: '5px',
|
|
||||||
'&:nth-of-type(odd)': {
|
|
||||||
backgroundColor: theme.palette.code.background,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,40 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
eventEntry: {
|
|
||||||
border: `1px solid ${theme.palette.neutral.light}`,
|
|
||||||
padding: '1rem',
|
|
||||||
margin: '1rem 0',
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
},
|
|
||||||
history: {
|
|
||||||
'& code': {
|
|
||||||
wordWrap: 'break-word',
|
|
||||||
whiteSpace: 'pre',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
lineHeight: '100%',
|
|
||||||
color: theme.palette.code.main,
|
|
||||||
},
|
|
||||||
'& code > .diff-N': {
|
|
||||||
color: theme.palette.code.diffAdd,
|
|
||||||
},
|
|
||||||
'& code > .diff-D': {
|
|
||||||
color: theme.palette.code.diffSub,
|
|
||||||
},
|
|
||||||
'& code > .diff-A, .diff-E': {
|
|
||||||
color: theme.palette.code.diffNeutral,
|
|
||||||
},
|
|
||||||
'& dl': {
|
|
||||||
padding: '0',
|
|
||||||
},
|
|
||||||
'& dt': {
|
|
||||||
float: 'left',
|
|
||||||
clear: 'left',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
'& dd': {
|
|
||||||
margin: '0 0 0 83px',
|
|
||||||
padding: '0 0 0.5em 0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,90 +0,0 @@
|
|||||||
import { List, Switch, FormControlLabel } from '@mui/material';
|
|
||||||
import EventJson from 'component/history/EventLog/EventJson/EventJson';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import EventCard from 'component/history/EventLog/EventCard/EventCard';
|
|
||||||
import { useStyles } from './EventLog.styles';
|
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
|
||||||
import { IEventSettings } from 'hooks/useEventSettings';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
|
||||||
import React from 'react';
|
|
||||||
import { ILocationSettings } from 'hooks/useLocationSettings';
|
|
||||||
|
|
||||||
interface IEventLogProps {
|
|
||||||
title: string;
|
|
||||||
events: IEvent[];
|
|
||||||
eventSettings: IEventSettings;
|
|
||||||
setEventSettings: React.Dispatch<React.SetStateAction<IEventSettings>>;
|
|
||||||
locationSettings: ILocationSettings;
|
|
||||||
displayInline?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EventLog = ({
|
|
||||||
title,
|
|
||||||
events,
|
|
||||||
eventSettings,
|
|
||||||
setEventSettings,
|
|
||||||
locationSettings,
|
|
||||||
displayInline,
|
|
||||||
}: IEventLogProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
const toggleShowDiff = () => {
|
|
||||||
setEventSettings({ showData: !eventSettings.showData });
|
|
||||||
};
|
|
||||||
const formatFulldateTime = (v: string) => {
|
|
||||||
return formatDateYMDHMS(v, locationSettings.locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!events || events.length < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries;
|
|
||||||
|
|
||||||
const renderListItemCards = (entry: IEvent) => (
|
|
||||||
<li key={entry.id} className={styles.eventEntry}>
|
|
||||||
<EventCard
|
|
||||||
entry={entry}
|
|
||||||
timeFormatted={formatFulldateTime(entry.createdAt)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (eventSettings.showData) {
|
|
||||||
entries = events.map(entry => (
|
|
||||||
<EventJson key={`log${entry.id}`} entry={entry} />
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
entries = events.map(renderListItemCards);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
disablePadding={displayInline}
|
|
||||||
disableBorder={displayInline}
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title={title}
|
|
||||||
actions={
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={eventSettings.showData}
|
|
||||||
onChange={toggleShowDiff}
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Full events"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.history}>
|
|
||||||
<List>{entries}</List>
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventLog;
|
|
@ -1,28 +0,0 @@
|
|||||||
import EventLog from 'component/history/EventLog/EventLog';
|
|
||||||
import { useEventSettings } from 'hooks/useEventSettings';
|
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
|
||||||
|
|
||||||
interface IEventLogContainerProps {
|
|
||||||
title: string;
|
|
||||||
events: IEvent[];
|
|
||||||
displayInline?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EventLogContainer = (props: IEventLogContainerProps) => {
|
|
||||||
const { locationSettings } = useLocationSettings();
|
|
||||||
const { eventSettings, setEventSettings } = useEventSettings();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventLog
|
|
||||||
title={props.title}
|
|
||||||
events={props.events}
|
|
||||||
eventSettings={eventSettings}
|
|
||||||
setEventSettings={setEventSettings}
|
|
||||||
locationSettings={locationSettings}
|
|
||||||
displayInline={props.displayInline}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventLogContainer;
|
|
@ -1,18 +0,0 @@
|
|||||||
import EventLog from '../EventLog';
|
|
||||||
import { useFeatureEvents } from 'hooks/api/getters/useFeatureEvents/useFeatureEvents';
|
|
||||||
|
|
||||||
interface IFeatureEventHistoryProps {
|
|
||||||
featureId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FeatureEventHistory = ({
|
|
||||||
featureId,
|
|
||||||
}: IFeatureEventHistoryProps) => {
|
|
||||||
const { events } = useFeatureEvents(featureId);
|
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EventLog events={events} title="Event log" displayInline />;
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FeatureEventHistory } from 'component/history/FeatureEventHistory/FeatureEventHistory';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
|
|
||||||
export const FeatureEventHistoryPage = () => {
|
|
||||||
const toggleName = useRequiredPathParam('toggleName');
|
|
||||||
|
|
||||||
return <FeatureEventHistory featureId={toggleName} />;
|
|
||||||
};
|
|
@ -319,14 +319,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Segments",
|
"title": "Segments",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"menu": {},
|
|
||||||
"parent": "/history",
|
|
||||||
"path": "/history/:toggleName",
|
|
||||||
"title": ":toggleName",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"menu": {
|
"menu": {
|
||||||
|
@ -39,8 +39,7 @@ import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectF
|
|||||||
import { CreateAddon } from 'component/addons/CreateAddon/CreateAddon';
|
import { CreateAddon } from 'component/addons/CreateAddon/CreateAddon';
|
||||||
import { EditAddon } from 'component/addons/EditAddon/EditAddon';
|
import { EditAddon } from 'component/addons/EditAddon/EditAddon';
|
||||||
import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature';
|
import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature';
|
||||||
import { EventHistoryPage } from 'component/history/EventHistoryPage/EventHistoryPage';
|
import { EventPage } from 'component/events/EventPage/EventPage';
|
||||||
import { FeatureEventHistoryPage } from 'component/history/FeatureEventHistoryPage/FeatureEventHistoryPage';
|
|
||||||
import { CreateStrategy } from 'component/strategies/CreateStrategy/CreateStrategy';
|
import { CreateStrategy } from 'component/strategies/CreateStrategy/CreateStrategy';
|
||||||
import { EditStrategy } from 'component/strategies/EditStrategy/EditStrategy';
|
import { EditStrategy } from 'component/strategies/EditStrategy/EditStrategy';
|
||||||
import { SplashPage } from 'component/splash/SplashPage/SplashPage';
|
import { SplashPage } from 'component/splash/SplashPage/SplashPage';
|
||||||
@ -363,18 +362,10 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// History
|
// History
|
||||||
{
|
|
||||||
path: '/history/:toggleName',
|
|
||||||
title: ':toggleName',
|
|
||||||
parent: '/history',
|
|
||||||
component: FeatureEventHistoryPage,
|
|
||||||
type: 'protected',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/history',
|
path: '/history',
|
||||||
title: 'Event log',
|
title: 'Event log',
|
||||||
component: EventHistoryPage,
|
component: EventPage,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
|||||||
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
|
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground';
|
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 { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm';
|
import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm';
|
||||||
import {
|
import {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { colors } from 'themes/colors';
|
import { colors } from 'themes/colors';
|
||||||
import { Alert, styled } from '@mui/material';
|
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 {
|
interface IContextBannerProps {
|
||||||
environment: string;
|
environment: string;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
PlaygroundFeatureSchema,
|
PlaygroundFeatureSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from '../../../../../../hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { Alert, IconButton, Typography, useTheme } from '@mui/material';
|
import { Alert, IconButton, Typography, useTheme } from '@mui/material';
|
||||||
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
|
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
|
||||||
import { useStyles } from './FeatureDetails.styles';
|
import { useStyles } from './FeatureDetails.styles';
|
||||||
import { CloseOutlined } from '@mui/icons-material';
|
import { CloseOutlined } from '@mui/icons-material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
checkForEmptyValues,
|
checkForEmptyValues,
|
||||||
hasCustomStrategies,
|
hasCustomStrategies,
|
||||||
@ -31,13 +31,17 @@ export const FeatureDetails = ({
|
|||||||
? `This feature toggle is True in ${input?.environment} because `
|
? `This feature toggle is True in ${input?.environment} because `
|
||||||
: `This feature toggle is False in ${input?.environment} because `;
|
: `This feature toggle is False in ${input?.environment} because `;
|
||||||
|
|
||||||
const reason = feature.isEnabled
|
const reason = (() => {
|
||||||
? 'at least one strategy is True'
|
if (feature.isEnabled) return 'at least one strategy is True';
|
||||||
: !feature.isEnabledInCurrentEnvironment
|
|
||||||
? 'the environment is disabled'
|
if (!feature.isEnabledInCurrentEnvironment)
|
||||||
: hasOnlyCustomStrategies(feature)
|
return 'the environment is disabled';
|
||||||
? 'no strategies could be fully evaluated'
|
|
||||||
: 'all strategies are either False or could not be fully evaluated';
|
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
|
const color = feature.isEnabled
|
||||||
? theme.palette.success.main
|
? theme.palette.success.main
|
||||||
@ -48,7 +52,7 @@ export const FeatureDetails = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const customStrategiesTxt = hasCustomStrategies(feature)
|
const customStrategiesTxt = hasCustomStrategies(feature)
|
||||||
? `You have custom strategies. Custom strategies can't be evaluated and they will be set as Unevaluated`
|
? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked as Unevaluated`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const onCloseClick =
|
const onCloseClick =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PlaygroundFeatureSchema } from '../../../../../../hooks/api/actions/usePlayground/playground.model';
|
import { PlaygroundFeatureSchema } from 'component/playground/Playground/interfaces/playground.model';
|
||||||
|
|
||||||
export const DEFAULT_STRATEGIES = [
|
export const DEFAULT_STRATEGIES = [
|
||||||
'default',
|
'default',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
PlaygroundFeatureSchema,
|
PlaygroundFeatureSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { IconButton, Popover, styled } from '@mui/material';
|
import { IconButton, Popover, styled } from '@mui/material';
|
||||||
import { InfoOutlined } from '@mui/icons-material';
|
import { InfoOutlined } from '@mui/icons-material';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
@ -3,7 +3,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import {
|
import {
|
||||||
PlaygroundFeatureSchema,
|
PlaygroundFeatureSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
|
|
||||||
interface PlaygroundResultFeatureStrategyListProps {
|
interface PlaygroundResultFeatureStrategyListProps {
|
||||||
|
@ -3,7 +3,7 @@ import { PlaygroundResultChip } from '../../../../PlaygroundResultChip/Playgroun
|
|||||||
import {
|
import {
|
||||||
PlaygroundStrategySchema,
|
PlaygroundStrategySchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
||||||
import { useStyles } from './FeatureStrategyItem.styles';
|
import { useStyles } from './FeatureStrategyItem.styles';
|
||||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
||||||
|
@ -17,7 +17,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
fill: '#fff',
|
fill: '#fff',
|
||||||
},
|
},
|
||||||
accordion: {
|
accordion: {
|
||||||
border: `1px solid ${theme.palette.grey[400]}`,
|
border: `1px solid ${theme.palette.dividerAlternative}`,
|
||||||
borderRadius: theme.spacing(1),
|
borderRadius: theme.spacing(1),
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
|
@ -18,7 +18,7 @@ import { useStyles } from './ConstraintAccordion.styles';
|
|||||||
import {
|
import {
|
||||||
PlaygroundConstraintSchema,
|
PlaygroundConstraintSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { ConstraintAccordionViewBody } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
import { ConstraintAccordionViewBody } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
||||||
|
|
||||||
interface IConstraintAccordionViewProps {
|
interface IConstraintAccordionViewProps {
|
||||||
|
@ -4,7 +4,7 @@ import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccord
|
|||||||
import {
|
import {
|
||||||
PlaygroundConstraintSchema,
|
PlaygroundConstraintSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
|
|
||||||
interface PlaygroundConstraintAccordionViewHeaderProps {
|
interface PlaygroundConstraintAccordionViewHeaderProps {
|
||||||
constraint: PlaygroundConstraintSchema;
|
constraint: PlaygroundConstraintSchema;
|
||||||
|
@ -8,7 +8,7 @@ import { CancelOutlined } from '@mui/icons-material';
|
|||||||
import {
|
import {
|
||||||
PlaygroundConstraintSchema,
|
PlaygroundConstraintSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { ConstraintViewHeaderOperator } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator';
|
import { ConstraintViewHeaderOperator } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator';
|
||||||
|
|
||||||
const StyledHeaderText = styled('span')(({ theme }) => ({
|
const StyledHeaderText = styled('span')(({ theme }) => ({
|
||||||
|
@ -3,7 +3,7 @@ import { styled, Typography } from '@mui/material';
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||||
import { PlaygroundConstraintSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
|
||||||
|
|
||||||
const StyledValuesSpan = styled('span')(({ theme }) => ({
|
const StyledValuesSpan = styled('span')(({ theme }) => ({
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
|
@ -3,7 +3,7 @@ import { Chip, styled, Typography } from '@mui/material';
|
|||||||
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { PlaygroundConstraintSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
|
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
|
||||||
|
@ -2,7 +2,7 @@ import { Fragment, VFC } from 'react';
|
|||||||
import {
|
import {
|
||||||
PlaygroundConstraintSchema,
|
PlaygroundConstraintSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { objectId } from 'utils/objectId';
|
import { objectId } from 'utils/objectId';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
@ -9,7 +9,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
import { Chip } from '@mui/material';
|
import { Chip } from '@mui/material';
|
||||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||||
import { PlaygroundConstraintSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
|
|
||||||
interface ICustomStrategyProps {
|
interface ICustomStrategyProps {
|
||||||
|
@ -32,7 +32,7 @@ export const PlaygroundParameterItem = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1" color={theme.palette[color].main}>
|
<Typography variant="subtitle1" color={theme.palette[color].main}>
|
||||||
{input}
|
{`${input}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.column}>
|
<div className={styles.column}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -2,7 +2,7 @@ import { VFC } from 'react';
|
|||||||
import {
|
import {
|
||||||
PlaygroundSegmentSchema,
|
PlaygroundSegmentSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
|
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
|
||||||
import { CancelOutlined, DonutLarge } from '@mui/icons-material';
|
import { CancelOutlined, DonutLarge } from '@mui/icons-material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -6,7 +6,7 @@ import { useStyles } from './StrategyExecution.styles';
|
|||||||
import {
|
import {
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
PlaygroundStrategySchema,
|
PlaygroundStrategySchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
|
import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
|
||||||
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
|
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
|
||||||
|
@ -10,7 +10,7 @@ import { useStyles } from '../StrategyExecution.styles';
|
|||||||
import {
|
import {
|
||||||
PlaygroundConstraintSchema,
|
PlaygroundConstraintSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { getMappedParam } from '../helpers';
|
import { getMappedParam } from '../helpers';
|
||||||
|
|
||||||
export interface PlaygroundResultStrategyExecutionParametersProps {
|
export interface PlaygroundResultStrategyExecutionParametersProps {
|
||||||
@ -88,18 +88,8 @@ export const PlaygroundResultStrategyExecutionParameters = ({
|
|||||||
key={key}
|
key={key}
|
||||||
value={hosts}
|
value={hosts}
|
||||||
text={'host'}
|
text={'host'}
|
||||||
input={
|
input={'no value'}
|
||||||
Boolean(input?.context?.[getMappedParam(key)])
|
showReason={undefined}
|
||||||
? input?.context?.[getMappedParam(key)]
|
|
||||||
: 'no value'
|
|
||||||
}
|
|
||||||
showReason={
|
|
||||||
Boolean(input?.context?.[getMappedParam(key)])
|
|
||||||
? !hosts.includes(
|
|
||||||
input?.context?.[getMappedParam(key)]
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'IPs':
|
case 'IPs':
|
||||||
|
@ -4,7 +4,6 @@ export const getMappedParam = (key: string) => {
|
|||||||
return 'userId';
|
return 'userId';
|
||||||
case 'IPS':
|
case 'IPS':
|
||||||
return 'remoteAddress';
|
return 'remoteAddress';
|
||||||
case 'HOSTNAMES':
|
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
PlaygroundFeatureSchema,
|
PlaygroundFeatureSchema,
|
||||||
PlaygroundStrategySchema,
|
PlaygroundStrategySchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
|
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
|
||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip';
|
import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip';
|
||||||
|
import { PlaygroundFeatureSchema } from '../../interfaces/playground.model';
|
||||||
|
|
||||||
interface IFeatureStatusCellProps {
|
interface IFeatureStatusCellProps {
|
||||||
enabled: boolean;
|
feature: PlaygroundFeatureSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledCellBox = styled(Box)(({ theme }) => ({
|
const StyledCellBox = styled(Box)(({ theme }) => ({
|
||||||
@ -16,13 +17,25 @@ const StyledChipWrapper = styled(Box)(() => ({
|
|||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
|
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 (
|
return (
|
||||||
<StyledCellBox>
|
<StyledCellBox>
|
||||||
<StyledChipWrapper data-loading>
|
<StyledChipWrapper data-loading>
|
||||||
<PlaygroundResultChip
|
<PlaygroundResultChip
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
label={enabled ? 'True' : 'False'}
|
label={label}
|
||||||
|
showIcon={enabled !== 'unknown'}
|
||||||
|
size={'medium'}
|
||||||
/>
|
/>
|
||||||
</StyledChipWrapper>
|
</StyledChipWrapper>
|
||||||
</StyledCellBox>
|
</StyledCellBox>
|
||||||
|
@ -22,7 +22,7 @@ import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
|
|||||||
import {
|
import {
|
||||||
PlaygroundFeatureSchema,
|
PlaygroundFeatureSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
} from 'hooks/api/actions/usePlayground/playground.model';
|
} from 'component/playground/Playground/interfaces/playground.model';
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from '@mui/material';
|
import { Box, Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { VariantCell } from './VariantCell/VariantCell';
|
import { VariantCell } from './VariantCell/VariantCell';
|
||||||
@ -108,7 +108,9 @@ export const PlaygroundResultsTable = ({
|
|||||||
accessor: 'isEnabled',
|
accessor: 'isEnabled',
|
||||||
filterName: 'isEnabled',
|
filterName: 'isEnabled',
|
||||||
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
|
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
|
||||||
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
|
Cell: ({ row }: any) => (
|
||||||
|
<FeatureStatusCell feature={row.original} />
|
||||||
|
),
|
||||||
sortType: 'boolean',
|
sortType: 'boolean',
|
||||||
sortInverted: true,
|
sortInverted: true,
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* 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 { VariantSchema } from 'openapi';
|
||||||
import { Operator } from 'constants/operators';
|
import { Operator } from 'constants/operators';
|
||||||
|
|
@ -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';
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
|
|
||||||
export const resolveProjects = (
|
export const resolveProjects = (
|
||||||
|
@ -2,7 +2,7 @@ import useAPI from '../useApi/useApi';
|
|||||||
import {
|
import {
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
PlaygroundResponseSchema,
|
PlaygroundResponseSchema,
|
||||||
} from './playground.model';
|
} from '../../../../component/playground/Playground/interfaces/playground.model';
|
||||||
|
|
||||||
export const usePlaygroundApi = () => {
|
export const usePlaygroundApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { IEvent } from 'interfaces/event';
|
||||||
|
|
||||||
|
const PATH = formatApiPath('api/admin/events/search');
|
||||||
|
|
||||||
|
export interface IUseEventSearchOutput {
|
||||||
|
events?: IEvent[];
|
||||||
|
fetchNextPage: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEventSearch {
|
||||||
|
type?: string;
|
||||||
|
project?: string;
|
||||||
|
feature?: string;
|
||||||
|
query?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEventSearch = (
|
||||||
|
project?: string,
|
||||||
|
feature?: string,
|
||||||
|
query?: string
|
||||||
|
): IUseEventSearchOutput => {
|
||||||
|
const [events, setEvents] = useState<IEvent[]>();
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
const search: IEventSearch = useMemo(
|
||||||
|
() => ({ project, feature, query, limit: 50 }),
|
||||||
|
[project, feature, query]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, isValidating } = useSWR<{ events: IEvent[] }>(
|
||||||
|
[PATH, search, offset],
|
||||||
|
() => searchEvents(PATH, { ...search, offset })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the page when there are new search conditions.
|
||||||
|
useEffect(() => {
|
||||||
|
setOffset(0);
|
||||||
|
setEvents(undefined);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
// Append results to the page when more data has been fetched.
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setEvents(prev => [...(prev ?? []), ...data.events]);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// Update the offset to fetch more results at the end of the page.
|
||||||
|
const fetchNextPage = useCallback(() => {
|
||||||
|
if (events && !isValidating) {
|
||||||
|
setOffset(events.length);
|
||||||
|
}
|
||||||
|
}, [events, isValidating]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
loading: !error && !data,
|
||||||
|
fetchNextPage,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchEvents = (path: string, search: IEventSearch) => {
|
||||||
|
return fetch(path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(search),
|
||||||
|
})
|
||||||
|
.then(handleErrorResponses('Event history'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
@ -1,39 +0,0 @@
|
|||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
|
||||||
|
|
||||||
const PATH = formatApiPath('api/admin/events');
|
|
||||||
|
|
||||||
export interface IUseEventsOutput {
|
|
||||||
events: IEvent[];
|
|
||||||
refetchEvents: () => void;
|
|
||||||
loading: boolean;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useEvents = (options?: SWRConfiguration): IUseEventsOutput => {
|
|
||||||
const { data, error } = useSWR<{ events: IEvent[] }>(
|
|
||||||
PATH,
|
|
||||||
fetchAllEvents,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetchEvents = useCallback(() => {
|
|
||||||
mutate(PATH).catch(console.warn);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
events: data?.events || [],
|
|
||||||
loading: !error && !data,
|
|
||||||
refetchEvents,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAllEvents = () => {
|
|
||||||
return fetch(PATH, { method: 'GET' })
|
|
||||||
.then(handleErrorResponses('Event history'))
|
|
||||||
.then(res => res.json());
|
|
||||||
};
|
|
@ -1,42 +0,0 @@
|
|||||||
import useSWR, { SWRConfiguration } from 'swr';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
|
||||||
import { IEvent } from 'interfaces/event';
|
|
||||||
|
|
||||||
const PATH = formatApiPath('api/admin/events');
|
|
||||||
|
|
||||||
export interface IUseEventsOutput {
|
|
||||||
events: IEvent[];
|
|
||||||
refetchEvents: () => void;
|
|
||||||
loading: boolean;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFeatureEvents = (
|
|
||||||
featureName: string,
|
|
||||||
options?: SWRConfiguration
|
|
||||||
): IUseEventsOutput => {
|
|
||||||
const { data, error, mutate } = useSWR<{ events: IEvent[] }>(
|
|
||||||
[PATH, featureName],
|
|
||||||
() => fetchFeatureEvents(featureName),
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetchEvents = useCallback(() => {
|
|
||||||
mutate().catch(console.warn);
|
|
||||||
}, [mutate]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
events: data?.events || [],
|
|
||||||
loading: !error && !data,
|
|
||||||
refetchEvents,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchFeatureEvents = (featureName: string) => {
|
|
||||||
return fetch(`${PATH}/${featureName}`, { method: 'GET' })
|
|
||||||
.then(handleErrorResponses('Event history'))
|
|
||||||
.then(res => res.json());
|
|
||||||
};
|
|
@ -1,41 +0,0 @@
|
|||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
|
||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
|
|
||||||
const useUiBootstrap = (options: SWRConfiguration = {}) => {
|
|
||||||
// The point of the bootstrap is to get multiple datasets in one call. Therefore,
|
|
||||||
// this needs to be refactored to seed other hooks with the correct data.
|
|
||||||
const BOOTSTRAP_CACHE_KEY = `api/admin/ui-bootstrap`;
|
|
||||||
|
|
||||||
const fetcher = () => {
|
|
||||||
const path = formatApiPath(`api/admin/ui-bootstrap`);
|
|
||||||
|
|
||||||
return fetch(path, {
|
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then(handleErrorResponses('ui bootstrap'))
|
|
||||||
.then(res => res.json());
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error } = useSWR(BOOTSTRAP_CACHE_KEY, fetcher, options);
|
|
||||||
const [loading, setLoading] = useState(!error && !data);
|
|
||||||
|
|
||||||
const refetchUiBootstrap = () => {
|
|
||||||
mutate(BOOTSTRAP_CACHE_KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(!error && !data);
|
|
||||||
}, [data, error]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
bootstrap: data,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
refetchUiBootstrap,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useUiBootstrap;
|
|
@ -9,7 +9,7 @@ const useHiddenColumns = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hidden = condition ? hiddenColumns : [];
|
const hidden = condition ? hiddenColumns : [];
|
||||||
setHiddenColumns(hidden);
|
setHiddenColumns(hidden);
|
||||||
}, [setHiddenColumns, condition]);
|
}, [setHiddenColumns, hiddenColumns, condition]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useHiddenColumns;
|
export default useHiddenColumns;
|
||||||
|
24
frontend/src/hooks/useOnVisible.ts
Normal file
24
frontend/src/hooks/useOnVisible.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Call `onVisible` when the `ref` element is fully visible in the viewport.
|
||||||
|
// Useful for detecting when the user has scrolled to the bottom of the page.
|
||||||
|
export const useOnVisible = <T extends HTMLElement>(onVisible: () => void) => {
|
||||||
|
const ref = useRef<T>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
const handler = (entries: IntersectionObserverEntry[]) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
onVisible();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(handler);
|
||||||
|
observer.observe(ref.current);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
|
}, [onVisible]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
};
|
@ -12,6 +12,7 @@ export interface IUiConfig {
|
|||||||
versionInfo?: IVersionInfo;
|
versionInfo?: IVersionInfo;
|
||||||
links: ILinks[];
|
links: ILinks[];
|
||||||
disablePasswordAuth?: boolean;
|
disablePasswordAuth?: boolean;
|
||||||
|
emailEnabled?: boolean;
|
||||||
toast?: IProclamationToast;
|
toast?: IProclamationToast;
|
||||||
segmentValuesLimit?: number;
|
segmentValuesLimit?: number;
|
||||||
strategySegmentsLimit?: number;
|
strategySegmentsLimit?: number;
|
||||||
|
@ -120,11 +120,10 @@ export default createTheme({
|
|||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
main: '#0b8c8f',
|
main: '#0b8c8f',
|
||||||
diffAdd: 'green',
|
diffAdd: '#3b6600',
|
||||||
diffSub: 'red',
|
diffSub: '#d11525',
|
||||||
diffNeutral: 'black',
|
diffNeutral: 'black',
|
||||||
edited: 'blue',
|
edited: 'black',
|
||||||
background: '#efefef',
|
|
||||||
},
|
},
|
||||||
activityIndicators: {
|
activityIndicators: {
|
||||||
unknown: colors.grey[100],
|
unknown: colors.grey[100],
|
||||||
|
@ -42,7 +42,6 @@ declare module '@mui/material/styles' {
|
|||||||
diffSub: string;
|
diffSub: string;
|
||||||
diffNeutral: string;
|
diffNeutral: string;
|
||||||
edited: string;
|
edited: string;
|
||||||
background: string;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* For 'Seen' column on feature toggles list and other.
|
* For 'Seen' column on feature toggles list and other.
|
||||||
|
Loading…
Reference in New Issue
Block a user