1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

Merge branch 'main' into task/Add_strategy_information_to_playground_results

This commit is contained in:
andreas-unleash 2022-08-09 18:04:14 +03:00 committed by GitHub
commit e46b75edf0
30 changed files with 408 additions and 486 deletions

View File

@ -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",

View File

@ -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

View File

@ -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);

View File

@ -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);

View 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;

View File

@ -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>
); );

View File

@ -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;

View 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>
);
};

View File

@ -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 />}
/> />
); );

View File

@ -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>
); );
}; };

View File

@ -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" />;
};

View File

@ -1,7 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()({
eventLogHeader: {
minWidth: '110px',
},
});

View File

@ -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;

View File

@ -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,
},
}));

View File

@ -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,
},
},
}));

View File

@ -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',
},
},
}));

View File

@ -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;

View File

@ -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;

View File

@ -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 />;
};

View File

@ -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} />;
};

View File

@ -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": {

View File

@ -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 },
}, },

View File

@ -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());
};

View File

@ -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());
};

View File

@ -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());
};

View File

@ -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;

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

View File

@ -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;

View File

@ -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],

View File

@ -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.