mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into task/Add_strategy_information_to_playground_results
This commit is contained in:
		
						commit
						e46b75edf0
					
				| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "unleash-frontend", | ||||
|   "description": "unleash your features", | ||||
|   "version": "4.14.3", | ||||
|   "version": "4.15.0-beta.0", | ||||
|   "keywords": [ | ||||
|     "unleash", | ||||
|     "feature toggle", | ||||
|  | ||||
| @ -13,7 +13,7 @@ import React from 'react'; | ||||
| import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { EDIT } from 'constants/misc'; | ||||
| import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 
 | ||||
| interface IUserForm { | ||||
|     email: string; | ||||
| @ -49,7 +49,7 @@ const UserForm: React.FC<IUserForm> = ({ | ||||
| }) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const { roles } = useUsers(); | ||||
|     const { bootstrap } = useUiBootstrap(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
| 
 | ||||
|     // @ts-expect-error
 | ||||
|     const sortRoles = (a, b) => { | ||||
| @ -127,7 +127,7 @@ const UserForm: React.FC<IUserForm> = ({ | ||||
|                     </RadioGroup> | ||||
|                 </FormControl> | ||||
|                 <ConditionallyRender | ||||
|                     condition={mode !== EDIT && bootstrap?.email} | ||||
|                     condition={mode !== EDIT && Boolean(uiConfig?.emailEnabled)} | ||||
|                     show={ | ||||
|                         <FormControl> | ||||
|                             <Typography | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap'; | ||||
| import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 
 | ||||
| const useCreateUserForm = ( | ||||
|     initialName = '', | ||||
|     initialEmail = '', | ||||
|     initialRootRole = 1 | ||||
| ) => { | ||||
|     const { bootstrap } = useUiBootstrap(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const [name, setName] = useState(initialName); | ||||
|     const [email, setEmail] = useState(initialEmail); | ||||
|     const [sendEmail, setSendEmail] = useState(false); | ||||
| @ -25,8 +25,8 @@ const useCreateUserForm = ( | ||||
|     }, [initialEmail]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setSendEmail(bootstrap?.email || false); | ||||
|     }, [bootstrap?.email]); | ||||
|         setSendEmail(uiConfig?.emailEnabled || false); | ||||
|     }, [uiConfig?.emailEnabled]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setRootRole(initialRootRole); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useRef, useState } from 'react'; | ||||
| import React, { useRef, useState } from 'react'; | ||||
| import { IconButton, InputBase, Tooltip } from '@mui/material'; | ||||
| import { Search as SearchIcon, Close } from '@mui/icons-material'; | ||||
| import classnames from 'classnames'; | ||||
| @ -18,6 +18,7 @@ interface ISearchProps { | ||||
|     disabled?: boolean; | ||||
|     getSearchContext?: () => IGetSearchContextOutput; | ||||
|     containerStyles?: React.CSSProperties; | ||||
|     debounceTime?: number; | ||||
| } | ||||
| 
 | ||||
| export const Search = ({ | ||||
| @ -29,14 +30,14 @@ export const Search = ({ | ||||
|     disabled, | ||||
|     getSearchContext, | ||||
|     containerStyles, | ||||
|     debounceTime = 200, | ||||
| }: ISearchProps) => { | ||||
|     const ref = useRef<HTMLInputElement>(); | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const [showSuggestions, setShowSuggestions] = useState(false); | ||||
| 
 | ||||
|     const [value, setValue] = useState(initialValue); | ||||
| 
 | ||||
|     const debouncedOnChange = useAsyncDebounce(onChange, 200); | ||||
|     const debouncedOnChange = useAsyncDebounce(onChange, debounceTime); | ||||
| 
 | ||||
|     const onSearchChange = (value: string) => { | ||||
|         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 { useStyles } from './EventDiff.styles'; | ||||
| import { IEvent } from 'interfaces/event'; | ||||
| import { useTheme } from '@mui/system'; | ||||
| import { CSSProperties } from 'react'; | ||||
| 
 | ||||
| const DIFF_PREFIXES = { | ||||
| const DIFF_PREFIXES: Record<string, string> = { | ||||
|     A: ' ', | ||||
|     E: ' ', | ||||
|     D: '-', | ||||
| @ -14,13 +15,13 @@ interface IEventDiffProps { | ||||
| } | ||||
| 
 | ||||
| const EventDiff = ({ entry }: IEventDiffProps) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     const KLASSES = { | ||||
|         A: styles.blue, // array edited
 | ||||
|         E: styles.blue, // edited
 | ||||
|         D: styles.negative, // deleted
 | ||||
|         N: styles.positive, // added
 | ||||
|     const styles: Record<string, CSSProperties> = { | ||||
|         A: { color: theme.palette.code.edited }, // array edited
 | ||||
|         E: { color: theme.palette.code.edited }, // edited
 | ||||
|         D: { color: theme.palette.code.diffSub }, // deleted
 | ||||
|         N: { color: theme.palette.code.diffAdd }, // added
 | ||||
|     }; | ||||
| 
 | ||||
|     const diffs = | ||||
| @ -32,18 +33,14 @@ const EventDiff = ({ entry }: IEventDiffProps) => { | ||||
|         let change; | ||||
|         if (diff.lhs !== undefined) { | ||||
|             change = ( | ||||
|                 <div> | ||||
|                     <div className={KLASSES.D}> | ||||
|                         - {key}: {JSON.stringify(diff.lhs)} | ||||
|                     </div> | ||||
|                 <div style={styles.D}> | ||||
|                     - {key}: {JSON.stringify(diff.lhs)} | ||||
|                 </div> | ||||
|             ); | ||||
|         } else if (diff.rhs !== undefined) { | ||||
|             change = ( | ||||
|                 <div> | ||||
|                     <div className={KLASSES.N}> | ||||
|                         + {key}: {JSON.stringify(diff.rhs)} | ||||
|                     </div> | ||||
|                 <div style={styles.N}> | ||||
|                     + {key}: {JSON.stringify(diff.rhs)} | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| @ -60,23 +57,19 @@ const EventDiff = ({ entry }: IEventDiffProps) => { | ||||
|         } else if (diff.lhs !== undefined && diff.rhs !== undefined) { | ||||
|             change = ( | ||||
|                 <div> | ||||
|                     <div className={KLASSES.D}> | ||||
|                     <div style={styles.D}> | ||||
|                         - {key}: {JSON.stringify(diff.lhs)} | ||||
|                     </div> | ||||
|                     <div className={KLASSES.N}> | ||||
|                     <div style={styles.N}> | ||||
|                         + {key}: {JSON.stringify(diff.rhs)} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             ); | ||||
|         } else { | ||||
|             // @ts-expect-error
 | ||||
|             const spadenClass = KLASSES[diff.kind]; | ||||
|             // @ts-expect-error
 | ||||
|             const prefix = DIFF_PREFIXES[diff.kind]; | ||||
| 
 | ||||
|             change = ( | ||||
|                 <div className={spadenClass}> | ||||
|                     {prefix} {key}: {JSON.stringify(diff.rhs || diff.item)} | ||||
|                 <div style={styles[diff.kind]}> | ||||
|                     {DIFF_PREFIXES[diff.kind]} {key}:{' '} | ||||
|                     {JSON.stringify(diff.rhs || diff.item)} | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| @ -91,16 +84,15 @@ const EventDiff = ({ entry }: IEventDiffProps) => { | ||||
|     } else { | ||||
|         // Just show the data if there is no diff yet.
 | ||||
|         const data = entry.data || entry.preData; | ||||
|         changes = ( | ||||
|             <div className={entry.data ? KLASSES.N : KLASSES.D}> | ||||
|         changes = [ | ||||
|             <div style={entry.data ? styles.N : styles.D}> | ||||
|                 {JSON.stringify(data, null, 2)} | ||||
|             </div> | ||||
|         ); | ||||
|             </div>, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <pre style={{ overflowX: 'auto', overflowY: 'hidden' }} tabIndex={0}> | ||||
|             {/* @ts-expect-error */} | ||||
|             <code>{changes.length === 0 ? '(no changes)' : changes}</code> | ||||
|         </pre> | ||||
|     ); | ||||
| @ -1,14 +1,25 @@ | ||||
| import PropTypes from 'prop-types'; | ||||
| import { useStyles } from './EventJson.styles'; | ||||
| import { IEvent } from 'interfaces/event'; | ||||
| import { styled } from '@mui/material'; | ||||
| 
 | ||||
| interface IEventJsonProps { | ||||
|     entry: IEvent; | ||||
| } | ||||
| 
 | ||||
| const EventJson = ({ entry }: IEventJsonProps) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
| export const StyledJsonListItem = styled('li')(({ theme }) => ({ | ||||
|     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)); | ||||
|     delete localEventData.description; | ||||
|     delete localEventData.name; | ||||
| @ -17,16 +28,12 @@ const EventJson = ({ entry }: IEventJsonProps) => { | ||||
|     const prettyPrinted = JSON.stringify(localEventData, null, 2); | ||||
| 
 | ||||
|     return ( | ||||
|         <li className={styles.historyItem}> | ||||
|         <StyledJsonListItem> | ||||
|             <div> | ||||
|                 <code>{prettyPrinted}</code> | ||||
|             </div> | ||||
|         </li> | ||||
|         </StyledJsonListItem> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| EventJson.propTypes = { | ||||
|     entry: PropTypes.object, | ||||
| }; | ||||
| 
 | ||||
| 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { EventHistory } from '../EventHistory/EventHistory'; | ||||
| import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; | ||||
| import { EventLog } from 'component/events/EventLog/EventLog'; | ||||
| 
 | ||||
| export const EventHistoryPage = () => { | ||||
| export const EventPage = () => { | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
| 
 | ||||
|     return ( | ||||
|         <ConditionallyRender | ||||
|             condition={hasAccess(ADMIN)} | ||||
|             show={<EventHistory />} | ||||
|             show={() => <EventLog title="Event log" />} | ||||
|             elseShow={<AdminAlert />} | ||||
|         /> | ||||
|     ); | ||||
| @ -1,7 +1,7 @@ | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import { useStyles } from './FeatureLog.styles'; | ||||
| import { FeatureEventHistory } from 'component/history/FeatureEventHistory/FeatureEventHistory'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { EventLog } from 'component/events/EventLog/EventLog'; | ||||
| 
 | ||||
| const FeatureLog = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
| @ -15,7 +15,12 @@ const FeatureLog = () => { | ||||
| 
 | ||||
|     return ( | ||||
|         <div className={styles.container}> | ||||
|             <FeatureEventHistory featureId={feature.name} /> | ||||
|             <EventLog | ||||
|                 title="Event log" | ||||
|                 project={projectId} | ||||
|                 feature={featureId} | ||||
|                 displayInline | ||||
|             /> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -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", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "menu": {}, | ||||
|     "parent": "/history", | ||||
|     "path": "/history/:toggleName", | ||||
|     "title": ":toggleName", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "menu": { | ||||
|  | ||||
| @ -39,8 +39,7 @@ import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectF | ||||
| import { CreateAddon } from 'component/addons/CreateAddon/CreateAddon'; | ||||
| import { EditAddon } from 'component/addons/EditAddon/EditAddon'; | ||||
| import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature'; | ||||
| import { EventHistoryPage } from 'component/history/EventHistoryPage/EventHistoryPage'; | ||||
| import { FeatureEventHistoryPage } from 'component/history/FeatureEventHistoryPage/FeatureEventHistoryPage'; | ||||
| import { EventPage } from 'component/events/EventPage/EventPage'; | ||||
| import { CreateStrategy } from 'component/strategies/CreateStrategy/CreateStrategy'; | ||||
| import { EditStrategy } from 'component/strategies/EditStrategy/EditStrategy'; | ||||
| import { SplashPage } from 'component/splash/SplashPage/SplashPage'; | ||||
| @ -363,18 +362,10 @@ export const routes: IRoute[] = [ | ||||
|     }, | ||||
| 
 | ||||
|     // History
 | ||||
|     { | ||||
|         path: '/history/:toggleName', | ||||
|         title: ':toggleName', | ||||
|         parent: '/history', | ||||
|         component: FeatureEventHistoryPage, | ||||
|         type: 'protected', | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/history', | ||||
|         title: 'Event log', | ||||
|         component: EventHistoryPage, | ||||
|         component: EventPage, | ||||
|         type: 'protected', | ||||
|         menu: { adminSettings: true }, | ||||
|     }, | ||||
|  | ||||
| @ -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; | ||||
							
								
								
									
										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; | ||||
|     links: ILinks[]; | ||||
|     disablePasswordAuth?: boolean; | ||||
|     emailEnabled?: boolean; | ||||
|     toast?: IProclamationToast; | ||||
|     segmentValuesLimit?: number; | ||||
|     strategySegmentsLimit?: number; | ||||
|  | ||||
| @ -120,11 +120,10 @@ export default createTheme({ | ||||
|         }, | ||||
|         code: { | ||||
|             main: '#0b8c8f', | ||||
|             diffAdd: 'green', | ||||
|             diffSub: 'red', | ||||
|             diffAdd: '#3b6600', | ||||
|             diffSub: '#d11525', | ||||
|             diffNeutral: 'black', | ||||
|             edited: 'blue', | ||||
|             background: '#efefef', | ||||
|             edited: 'black', | ||||
|         }, | ||||
|         activityIndicators: { | ||||
|             unknown: colors.grey[100], | ||||
|  | ||||
| @ -42,7 +42,6 @@ declare module '@mui/material/styles' { | ||||
|             diffSub: string; | ||||
|             diffNeutral: string; | ||||
|             edited: string; | ||||
|             background: string; | ||||
|         }; | ||||
|         /** | ||||
|          * For 'Seen' column on feature toggles list and other. | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user