1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

Fix/environment list sorting (#447)

* fix: wait for api call before refetching

* fix: set active environment from feature instead of cache

* fix: remove console logs

* fix: add permission icon button to project card

* fix: remove project button

* fix: empty tooltip if it is not passed

* fix: add refresh interval

* fix: permission buttons

* fix: project permission buttons

* fix: remove unused imports

* fix: add projectId
This commit is contained in:
Fredrik Strand Oseberg 2021-10-20 12:05:44 +02:00 committed by GitHub
parent f61a949df2
commit 57928d50c6
41 changed files with 310 additions and 404 deletions

View File

@ -21,6 +21,7 @@ export interface ISelectMenuProps {
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
classes?: any; classes?: any;
defaultValue?: string;
} }
const GeneralSelect: React.FC<ISelectMenuProps> = ({ const GeneralSelect: React.FC<ISelectMenuProps> = ({
@ -29,6 +30,7 @@ const GeneralSelect: React.FC<ISelectMenuProps> = ({
label = '', label = '',
options, options,
onChange, onChange,
defaultValue,
id, id,
disabled = false, disabled = false,
className, className,
@ -53,6 +55,7 @@ const GeneralSelect: React.FC<ISelectMenuProps> = ({
{label} {label}
</InputLabel> </InputLabel>
<Select <Select
defaultValue={defaultValue}
name={name} name={name}
disabled={disabled} disabled={disabled}
onChange={onChange} onChange={onChange}

View File

@ -10,6 +10,7 @@ interface IPermissionIconButtonProps extends OverridableComponent<any> {
tooltip: string; tooltip: string;
onClick?: (e: any) => void; onClick?: (e: any) => void;
disabled?: boolean; disabled?: boolean;
projectId?: string;
} }
const PermissionButton: React.FC<IPermissionIconButtonProps> = ({ const PermissionButton: React.FC<IPermissionIconButtonProps> = ({
@ -18,11 +19,15 @@ const PermissionButton: React.FC<IPermissionIconButtonProps> = ({
onClick, onClick,
children, children,
disabled, disabled,
projectId,
...rest ...rest
}) => { }) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const access = hasAccess(permission); const access = projectId
? hasAccess(permission, projectId)
: hasAccess(permission);
const tooltipText = access const tooltipText = access
? tooltip ? tooltip
: "You don't have access to perform this operation"; : "You don't have access to perform this operation";

View File

@ -8,6 +8,7 @@ interface IPermissionIconButtonProps extends OverridableComponent<any> {
Icon: React.ElementType; Icon: React.ElementType;
tooltip: string; tooltip: string;
onClick?: (e: any) => void; onClick?: (e: any) => void;
projectId?: string;
} }
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({ const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
@ -15,14 +16,18 @@ const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
Icon, Icon,
tooltip, tooltip,
onClick, onClick,
projectId,
children, children,
...rest ...rest
}) => { }) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const access = hasAccess(permission); const access = projectId
? hasAccess(permission, projectId)
: hasAccess(permission);
const tooltipText = access const tooltipText = access
? tooltip ? tooltip || ''
: "You don't have access to perform this operation"; : "You don't have access to perform this operation";
return ( return (

View File

@ -9,6 +9,7 @@ interface IResponsiveButtonProps {
tooltip?: string; tooltip?: string;
disabled?: boolean; disabled?: boolean;
permission?: string; permission?: string;
projectId?: string;
maxWidth: string; maxWidth: string;
} }
@ -20,6 +21,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
disabled = false, disabled = false,
children, children,
permission, permission,
projectId,
...rest ...rest
}) => { }) => {
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`); const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
@ -32,6 +34,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
permission={permission} permission={permission}
projectId={projectId}
data-loading data-loading
{...rest} {...rest}
> >
@ -42,6 +45,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
<PermissionButton <PermissionButton
onClick={onClick} onClick={onClick}
permission={permission} permission={permission}
projectId={projectId}
color="primary" color="primary"
variant="contained" variant="contained"
disabled={disabled} disabled={disabled}

View File

@ -70,6 +70,7 @@ const EnvironmentList = () => {
try { try {
await sortOrderAPICall(sortOrder); await sortOrderAPICall(sortOrder);
refetch();
} catch (e) { } catch (e) {
setToastData({ setToastData({
show: true, show: true,
@ -77,13 +78,11 @@ const EnvironmentList = () => {
text: e.toString(), text: e.toString(),
}); });
} }
mutate(ENVIRONMENT_CACHE_KEY);
}; };
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => { const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
try { try {
changeSortOrder(sortOrder); await changeSortOrder(sortOrder);
} catch (e) { } catch (e) {
setToastData({ setToastData({
show: true, show: true,

View File

@ -1,9 +1,5 @@
import { useRef } from 'react'; import { useContext, useRef } from 'react';
import { import { Switch, TableCell, TableRow } from '@material-ui/core';
Switch,
TableCell,
TableRow,
} from '@material-ui/core';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useStyles } from '../FeatureToggleListNew.styles'; import { useStyles } from '../FeatureToggleListNew.styles';
@ -17,6 +13,9 @@ import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus';
import FeatureType from '../../FeatureView2/FeatureType/FeatureType'; import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
import classNames from 'classnames'; import classNames from 'classnames';
import CreatedAt from './CreatedAt'; import CreatedAt from './CreatedAt';
import useProject from '../../../../hooks/api/getters/useProject/useProject';
import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import AccessContext from '../../../../contexts/AccessContext';
interface IFeatureToggleListNewItemProps { interface IFeatureToggleListNewItemProps {
name: string; name: string;
@ -35,14 +34,15 @@ const FeatureToggleListNewItem = ({
projectId, projectId,
createdAt, createdAt,
}: IFeatureToggleListNewItemProps) => { }: IFeatureToggleListNewItemProps) => {
const { hasAccess } = useContext(AccessContext);
const { toast, setToastData } = useToast(); const { toast, setToastData } = useToast();
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv( const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
projectId, projectId,
name, name
); );
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { refetch } = useProject(projectId);
const styles = useStyles(); const styles = useStyles();
const history = useHistory(); const history = useHistory();
const ref = useRef(null); const ref = useRef(null);
@ -61,6 +61,7 @@ const FeatureToggleListNewItem = ({
type: 'success', type: 'success',
text: 'Successfully updated toggle status.', text: 'Successfully updated toggle status.',
}); });
refetch();
}) })
.catch(e => { .catch(e => {
setToastData({ setToastData({
@ -71,43 +72,66 @@ const FeatureToggleListNewItem = ({
}); });
}; };
return ( return (
<> <>
<TableRow className={styles.tableRow}> <TableRow className={styles.tableRow}>
<TableCell className={classNames( <TableCell
styles.tableCell, className={classNames(
styles.tableCellStatus)} align="left" onClick={onClick}> styles.tableCell,
styles.tableCellStatus
)}
align="left"
onClick={onClick}
>
<FeatureStatus lastSeenAt={lastSeenAt} /> <FeatureStatus lastSeenAt={lastSeenAt} />
</TableCell> </TableCell>
<TableCell className={classNames( <TableCell
styles.tableCell, className={classNames(
styles.tableCellType)} align="center" onClick={onClick}> styles.tableCell,
styles.tableCellType
)}
align="center"
onClick={onClick}
>
<FeatureType type={type} /> <FeatureType type={type} />
</TableCell> </TableCell>
<TableCell className={classNames( <TableCell
styles.tableCell, styles.tableCellName)} align="left" onClick={onClick}> className={classNames(
styles.tableCell,
styles.tableCellName
)}
align="left"
onClick={onClick}
>
<span data-loading>{name}</span> <span data-loading>{name}</span>
</TableCell> </TableCell>
<TableCell className={classNames( <TableCell
styles.tableCell, styles.tableCellCreated)} align="left" onClick={onClick}> className={classNames(
<CreatedAt time={createdAt} /> styles.tableCell,
styles.tableCellCreated
)}
align="left"
onClick={onClick}
>
<CreatedAt time={createdAt} />
</TableCell> </TableCell>
{environments.map((env: IEnvironments) => { {environments.map((env: IEnvironments) => {
return ( return (
<TableCell <TableCell
className={classNames( className={classNames(
styles.tableCell, styles.tableCell,
styles.tableCellEnv)} styles.tableCellEnv
)}
align="center" align="center"
key={env.name} key={env.name}
> >
<span data-loading style={{ display: 'block' }}> <span data-loading style={{ display: 'block' }}>
<Switch <Switch
checked={env.enabled} checked={env.enabled}
disabled={
!hasAccess(UPDATE_FEATURE, projectId)
}
ref={ref} ref={ref}
onClick={handleToggle.bind(this, env)} onClick={handleToggle.bind(this, env)}
/> />

View File

@ -40,6 +40,7 @@ const FeatureOverviewStale = () => {
<PermissionButton <PermissionButton
onClick={() => setOpenStaleDialog(true)} onClick={() => setOpenStaleDialog(true)}
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Flip status" tooltip="Flip status"
variant="text" variant="text"
> >

View File

@ -37,6 +37,7 @@ const FeatureOverviewStrategies = () => {
tooltip="Add new strategy" tooltip="Add new strategy"
className={styles.addStrategyButton} className={styles.addStrategyButton}
component={Link} component={Link}
projectId={projectId}
to={`/projects/${projectId}/features2/${featureId}/strategies?addStrategy=true`} to={`/projects/${projectId}/features2/${featureId}/strategies?addStrategy=true`}
> >
Add new strategy Add new strategy

View File

@ -32,7 +32,7 @@ const FeatureOverviewTags = () => {
type: '', type: '',
}); });
const styles = useStyles(); const styles = useStyles();
const { featureId } = useParams<IFeatureViewParams>(); const { featureId, projectId } = useParams<IFeatureViewParams>();
const { tags, refetch } = useTags(featureId); const { tags, refetch } = useTags(featureId);
const { tagTypes } = useTagTypes(); const { tagTypes } = useTagTypes();
const { deleteTagFromFeature } = useFeatureApi(); const { deleteTagFromFeature } = useFeatureApi();
@ -131,6 +131,7 @@ const FeatureOverviewTags = () => {
<PermissionIconButton <PermissionIconButton
onClick={() => setOpenTagDialog(true)} onClick={() => setOpenTagDialog(true)}
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Add tag" tooltip="Add tag"
data-loading data-loading
> >

View File

@ -89,6 +89,7 @@ const FeatureSettingsMetadata = () => {
tooltip="Save changes" tooltip="Save changes"
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
onClick={handleSubmit} onClick={handleSubmit}
projectId={projectId}
> >
Save changes Save changes
</PermissionButton> </PermissionButton>

View File

@ -78,6 +78,7 @@ const FeatureSettingsProject = () => {
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
tooltip="Update feature" tooltip="Update feature"
onClick={() => setShowConfirmDialog(true)} onClick={() => setShowConfirmDialog(true)}
projectId={projectId}
> >
Save changes Save changes
</PermissionButton> </PermissionButton>

View File

@ -132,6 +132,7 @@ const FeatureStrategiesEnvironmentList = ({
const strategiesContainerClasses = classnames({ const strategiesContainerClasses = classnames({
[styles.strategiesContainer]: !expandedSidebar, [styles.strategiesContainer]: !expandedSidebar,
}); });
return ( return (
<ConditionallyRender <ConditionallyRender
condition={!configureNewStrategy} condition={!configureNewStrategy}

View File

@ -377,6 +377,7 @@ const FeatureStrategiesEnvironments = () => {
} }
Icon={Add} Icon={Add}
maxWidth="700px" maxWidth="700px"
projectId={projectId}
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
> >
Add new strategy Add new strategy
@ -390,7 +391,7 @@ const FeatureStrategiesEnvironments = () => {
onChange={(_, tabId) => { onChange={(_, tabId) => {
setActiveTab(tabId); setActiveTab(tabId);
setActiveEnvironment( setActiveEnvironment(
featureCache?.environments[tabId] feature?.environments[tabId]
); );
history.replace(history.location.pathname); history.replace(history.location.pathname);
}} }}

View File

@ -3,7 +3,6 @@ import * as jsonpatch from 'fast-json-patch';
import styles from './variants.module.scss'; import styles from './variants.module.scss';
import { import {
Button,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
@ -29,6 +28,7 @@ import useToast from '../../../../../hooks/useToast';
import { updateWeight } from '../../../../common/util'; import { updateWeight } from '../../../../common/util';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup'; import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
const FeatureOverviewVariants = () => { const FeatureOverviewVariants = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -275,28 +275,24 @@ const FeatureOverviewVariants = () => {
/> />
<br /> <br />
<ConditionallyRender
condition={editable} <div>
show={ <PermissionButton
<div> onClick={() => {
<Button setEditing(false);
title="Add variant" setEditVariant({});
onClick={() => { setShowAddVariant(true);
setEditing(false); }}
setEditVariant({}); className={styles.addVariantButton}
setShowAddVariant(true); data-test={'ADD_VARIANT_BUTTON'}
}} permission={UPDATE_FEATURE}
variant="contained" projectId={projectId}
color="primary" >
className={styles.addVariantButton} Add variant
data-test={'ADD_VARIANT_BUTTON'} </PermissionButton>
> {renderStickiness()}
Add variant </div>
</Button>
{renderStickiness()}
</div>
}
/>
<AddVariant <AddVariant
showDialog={showAddVariant} showDialog={showAddVariant}
closeDialog={handleCloseAddVariant} closeDialog={handleCloseAddVariant}
@ -320,187 +316,3 @@ const FeatureOverviewVariants = () => {
}; };
export default FeatureOverviewVariants; export default FeatureOverviewVariants;
// class UpdateVariantComponent extends Component {
// constructor(props) {
// super(props);
// this.state = { ...initialState };
// }
// closeDialog = () => {
// this.setState({ ...initialState });
// };
// openAddVariant = e => {
// e.preventDefault();
// this.setState({
// showDialog: true,
// editVariant: undefined,
// editIndex: undefined,
// title: 'Add variant',
// });
// };
// openEditVariant = (e, index, variant) => {
// e.preventDefault();
// if (this.props.editable) {
// this.setState({
// showDialog: true,
// editVariant: variant,
// editIndex: index,
// title: 'Edit variant',
// });
// }
// };
// validateName = name => {
// if (!name) {
// return { name: 'Name is required' };
// }
// };
// onRemoveVariant = (e, index) => {
// e.preventDefault();
// try {
// this.props.removeVariant(index);
// } catch (e) {
// console.log('An exception was caught.');
// }
// };
// renderVariant = (variant, index) => (
// <VariantViewComponent
// key={variant.name}
// variant={variant}
// editVariant={e => this.openEditVariant(e, index, variant)}
// removeVariant={e => this.onRemoveVariant(e, index)}
// editable={this.props.editable}
// />
// );
// renderVariants = variants => (
// <Table className={styles.variantTable}>
// <TableHead>
// <TableRow>
// <TableCell>Variant name</TableCell>
// <TableCell className={styles.labels} />
// <TableCell>Weight</TableCell>
// <TableCell>Weight Type</TableCell>
// <TableCell className={styles.actions} />
// </TableRow>
// </TableHead>
// <TableBody>{variants.map(this.renderVariant)}</TableBody>
// </Table>
// );
// renderStickiness = variants => {
// const { updateStickiness, stickinessOptions } = this.props;
// if (!variants || variants.length < 2) {
// return null;
// }
// const value = variants[0].stickiness || 'default';
// const options = stickinessOptions.map(c => ({ key: c, label: c }));
// // guard on stickiness being disabled for context field.
// if (!stickinessOptions.includes(value)) {
// options.push({ key: value, label: value });
// }
// const onChange = event => updateStickiness(event.target.value);
// return (
// <section style={{ paddingTop: '16px' }}>
// <GeneralSelect
// label="Stickiness"
// options={options}
// value={value}
// onChange={onChange}
// />
// &nbsp;&nbsp;
// <small
// className={classnames(styles.paragraph, styles.helperText)}
// style={{ display: 'block', marginTop: '0.5rem' }}
// >
// By overriding the stickiness you can control which parameter
// you want to be used in order to ensure consistent traffic
// allocation across variants.{' '}
// <a
// href="https://docs.getunleash.io/advanced/toggle_variants"
// target="_blank"
// rel="noreferrer"
// >
// Read more
// </a>
// </small>
// </section>
// );
// };
// render() {
// const { showDialog, editVariant, editIndex, title } = this.state;
// const { variants, addVariant, updateVariant } = this.props;
// const saveVariant = editVariant
// ? updateVariant.bind(null, editIndex)
// : addVariant;
// return (
// <section style={{ padding: '16px' }}>
// <Typography variant="body1">
// Variants allows you to return a variant object if the
// feature toggle is considered enabled for the current
// request. When using variants you should use the{' '}
// <code style={{ color: 'navy' }}>getVariant()</code> method
// in the Client SDK.
// </Typography>
// <ConditionallyRender
// condition={variants.length > 0}
// show={this.renderVariants(variants)}
// elseShow={<p>No variants defined.</p>}
// />
// <br />
// <ConditionallyRender
// condition={this.props.editable}
// show={
// <div>
// <Button
// title="Add variant"
// onClick={this.openAddVariant}
// variant="contained"
// color="primary"
// className={styles.addVariantButton}
// >
// Add variant
// </Button>
// {this.renderStickiness(variants)}
// </div>
// }
// />
// <AddVariant
// showDialog={showDialog}
// closeDialog={this.closeDialog}
// save={saveVariant}
// validateName={this.validateName}
// editVariant={editVariant}
// title={title}
// />
// </section>
// );
// }
// }
// UpdateVariantComponent.propTypes = {
// variants: PropTypes.array.isRequired,
// addVariant: PropTypes.func.isRequired,
// removeVariant: PropTypes.func.isRequired,
// updateVariant: PropTypes.func.isRequired,
// updateStickiness: PropTypes.func.isRequired,
// editable: PropTypes.bool.isRequired,
// stickinessOptions: PropTypes.array,
// };
// export default UpdateVariantComponent;

View File

@ -131,6 +131,7 @@ const FeatureView2 = () => {
<div className={styles.actions}> <div className={styles.actions}>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Copy" tooltip="Copy"
data-loading data-loading
component={Link} component={Link}
@ -140,6 +141,7 @@ const FeatureView2 = () => {
</PermissionIconButton> </PermissionIconButton>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Archive feature toggle" tooltip="Archive feature toggle"
data-loading data-loading
onClick={() => setShowDelDialog(true)} onClick={() => setShowDelDialog(true)}

View File

@ -68,12 +68,12 @@ const FeatureViewEnvironment: FC<IFeatureViewEnvironmentProps> = ({
} }
}; };
const toggleEnvironment = (e: React.ChangeEvent) => { const toggleEnvironment = async (e: React.ChangeEvent) => {
if (env.enabled) { if (env.enabled) {
handleToggleEnvironmentOff(); await handleToggleEnvironmentOff();
return; return;
} }
handleToggleEnvironmentOn(); await handleToggleEnvironmentOn();
}; };
const iconContainerClasses = classNames(styles.iconContainer, { const iconContainerClasses = classNames(styles.iconContainer, {
@ -100,8 +100,12 @@ const FeatureViewEnvironment: FC<IFeatureViewEnvironmentProps> = ({
<div className={iconContainerClasses}> <div className={iconContainerClasses}>
<Cloud className={iconClasses} /> <Cloud className={iconClasses} />
</div> </div>
<Tooltip title={`${env.name} is an environment of type "${env.type}".`}> <Tooltip
<p className={styles.environmentBadgeParagraph}>{env.name}</p> title={`${env.name} is an environment of type "${env.type}".`}
>
<p className={styles.environmentBadgeParagraph}>
{env.name}
</p>
</Tooltip> </Tooltip>
</div> </div>
@ -117,11 +121,15 @@ const FeatureViewEnvironment: FC<IFeatureViewEnvironmentProps> = ({
onChange={toggleEnvironment} onChange={toggleEnvironment}
/>{' '} />{' '}
<span className={styles.toggleText}> <span className={styles.toggleText}>
{env.name}{' environment is '} {env.name}
<strong>{env.enabled ? 'enabled' : 'disabled'}</strong> {' environment is '}
<strong>
{env.enabled ? 'enabled' : 'disabled'}
</strong>
</span> </span>
</div> </div>
} /> }
/>
</div> </div>
<div className={styles.environmentStatus} data-loading> <div className={styles.environmentStatus} data-loading>
<ConditionallyRender <ConditionallyRender
@ -138,7 +146,8 @@ const FeatureViewEnvironment: FC<IFeatureViewEnvironmentProps> = ({
Configure strategies for {env.name} Configure strategies for {env.name}
</Link> </Link>
</> </>
} /> }
/>
</div> </div>
</div> </div>

View File

@ -87,8 +87,11 @@ const CreateFeature = ({
</div> </div>
<section className={styles.formContainer}> <section className={styles.formContainer}>
<ProjectSelect <ProjectSelect
value={project || input.project} value={input.project}
onChange={v => setValue('project', v.target.value)} defaultValue={project}
onChange={v => {
setValue('project', v.target.value);
}}
filter={projectFilterGenerator(user, CREATE_FEATURE)} filter={projectFilterGenerator(user, CREATE_FEATURE)}
/> />
</section> </section>

View File

@ -11,7 +11,8 @@ class ProjectSelectComponent extends Component {
} }
render() { render() {
const { value, projects, onChange, enabled, filter } = this.props; const { value, projects, onChange, enabled, filter, defaultValue } =
this.props;
if (!enabled) { if (!enabled) {
return null; return null;
@ -43,6 +44,7 @@ class ProjectSelectComponent extends Component {
return ( return (
<GeneralSelect <GeneralSelect
label="Project" label="Project"
defaultValue={defaultValue}
options={options} options={options}
value={value} value={value}
onChange={onChange} onChange={onChange}

View File

@ -55,26 +55,24 @@ const ProjectFeatureToggles = ({
</IconButton> </IconButton>
} }
/> />
<ConditionallyRender
condition={hasAccess(CREATE_FEATURE, id)} <ResponsiveButton
show={ onClick={() =>
<ResponsiveButton history.push(
onClick={() => getCreateTogglePath(
history.push( id,
getCreateTogglePath( uiConfig.flags.E
id, )
uiConfig.flags.E )
)
)
}
maxWidth="700px"
tooltip="New feature toggle"
Icon={Add}
>
New feature toggle
</ResponsiveButton>
} }
/> maxWidth="700px"
tooltip="New feature toggle"
Icon={Add}
projectId={id}
permission={CREATE_FEATURE}
>
New feature toggle
</ResponsiveButton>
</> </>
} }
/> />

View File

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
projectInfo: { projectInfo: {
width: '275px', width: '225px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',

View File

@ -8,7 +8,9 @@ interface ProjectOverviewProps {
} }
const ProjectOverview = ({ projectId }: ProjectOverviewProps) => { const ProjectOverview = ({ projectId }: ProjectOverviewProps) => {
const { project, loading } = useProject(projectId); const { project, loading } = useProject(projectId, {
refreshInterval: 10000,
});
const { members, features, health } = project; const { members, features, health } = project;
const styles = useStyles(); const styles = useStyles();

View File

@ -1,10 +1,9 @@
import { Card, IconButton, Menu, MenuItem } from '@material-ui/core'; import { Card, Menu, MenuItem } from '@material-ui/core';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useStyles } from './ProjectCard.styles'; import { useStyles } from './ProjectCard.styles';
import MoreVertIcon from '@material-ui/icons/MoreVert'; import MoreVertIcon from '@material-ui/icons/MoreVert';
import { ReactComponent as ProjectIcon } from '../../../assets/icons/projectIcon.svg'; import { ReactComponent as ProjectIcon } from '../../../assets/icons/projectIcon.svg';
import ConditionallyRender from '../../common/ConditionallyRender';
import { useState } from 'react'; import { useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import Dialogue from '../../common/Dialogue'; import Dialogue from '../../common/Dialogue';
@ -12,6 +11,8 @@ import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectAp
import useProjects from '../../../hooks/api/getters/useProjects/useProjects'; import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
import { Delete, Edit } from '@material-ui/icons'; import { Delete, Edit } from '@material-ui/icons';
import { getProjectEditPath } from '../../../utils/route-path-helpers'; import { getProjectEditPath } from '../../../utils/route-path-helpers';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from '../../../store/project/actions';
interface IProjectCardProps { interface IProjectCardProps {
name: string; name: string;
featureCount: number; featureCount: number;
@ -53,18 +54,17 @@ const ProjectCard = ({
<Card className={styles.projectCard} onMouseEnter={onHover}> <Card className={styles.projectCard} onMouseEnter={onHover}>
<div className={styles.header} data-loading> <div className={styles.header} data-loading>
<h2 className={styles.title}>{name}</h2> <h2 className={styles.title}>{name}</h2>
<ConditionallyRender
condition={true} <PermissionIconButton
show={ permission={UPDATE_PROJECT}
<IconButton projectId={id}
className={styles.actionsBtn} className={styles.actionsBtn}
data-loading data-loading
onClick={handleClick} onClick={handleClick}
> >
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </PermissionIconButton>
}
/>
<Menu <Menu
id="project-card-menu" id="project-card-menu"
open={Boolean(anchorEl)} open={Boolean(anchorEl)}

View File

@ -135,22 +135,16 @@ const ProjectListNew = () => {
<HeaderTitle <HeaderTitle
title="Projects" title="Projects"
actions={ actions={
<ConditionallyRender <ResponsiveButton
condition={hasAccess(CREATE_PROJECT)} Icon={Add}
show={ onClick={() => history.push('/projects/create')}
<ResponsiveButton maxWidth="700px"
Icon={Add} permission={CREATE_PROJECT}
onClick={() => tooltip={createButtonData.title}
history.push('/projects/create') disabled={createButtonData.disabled}
} >
maxWidth="700px" Add new project
tooltip={createButtonData.title} </ResponsiveButton>
disabled={createButtonData.disabled}
>
Add new project
</ResponsiveButton>
}
/>
} }
/> />
} }

View File

@ -15,6 +15,8 @@ import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import { FormEvent } from 'react-router/node_modules/@types/react'; import { FormEvent } from 'react-router/node_modules/@types/react';
import useLoading from '../../hooks/useLoading'; import useLoading from '../../hooks/useLoading';
import PermissionButton from '../common/PermissionButton/PermissionButton';
import { UPDATE_PROJECT } from '../../store/project/actions';
interface ProjectFormComponentProps { interface ProjectFormComponentProps {
editMode: boolean; editMode: boolean;
@ -106,7 +108,7 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
<PageContent <PageContent
headerContent={ headerContent={
<HeaderTitle <HeaderTitle
title={`${submitText} ${project?.name} project`} title={`${submitText} ${props.project?.name} project`}
/> />
} }
> >
@ -189,7 +191,9 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
/> />
<ConditionallyRender <ConditionallyRender
condition={hasAccess(CREATE_PROJECT)} condition={
hasAccess(CREATE_PROJECT) && !editMode
}
show={ show={
<div className={styles.formButtons}> <div className={styles.formButtons}>
<FormButtons <FormButtons
@ -199,6 +203,20 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
</div> </div>
} }
/> />
<ConditionallyRender
condition={editMode}
show={
<PermissionButton
permission={UPDATE_PROJECT}
projectId={props.project.id}
type="submit"
style={{ marginTop: '1rem' }}
>
Update project
</PermissionButton>
}
/>
</form> </form>
</> </>
} }

View File

@ -1,9 +1,9 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useApiTokens = () => { const useApiTokens = (options: SWRConfiguration = {}) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/api-tokens`); const path = formatApiPath(`api/admin/api-tokens`);
const res = await fetch(path, { const res = await fetch(path, {
@ -14,7 +14,7 @@ const useApiTokens = () => {
const KEY = `api/admin/api-tokens`; const KEY = `api/admin/api-tokens`;
const { data, error } = useSWR(KEY, fetcher); const { data, error } = useSWR(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { IEnvironmentResponse } from '../../../../interfaces/environments'; import { IEnvironmentResponse } from '../../../../interfaces/environments';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
@ -6,17 +6,20 @@ import handleErrorResponses from '../httpErrorResponseHandler';
export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`; export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`;
const useEnvironments = () => { const useEnvironments = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/environments`); const path = formatApiPath(`api/admin/environments`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Environments')).then(res => res.json()); })
.then(handleErrorResponses('Environments'))
.then(res => res.json());
}; };
const { data, error } = useSWR<IEnvironmentResponse>( const { data, error } = useSWR<IEnvironmentResponse>(
ENVIRONMENT_CACHE_KEY, ENVIRONMENT_CACHE_KEY,
fetcher fetcher,
options
); );
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
@ -6,17 +6,10 @@ import { IFeatureToggle } from '../../../../interfaces/featureToggle';
import { defaultFeature } from './defaultFeature'; import { defaultFeature } from './defaultFeature';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions {
refreshInterval?: number;
revalidateOnFocus?: boolean;
revalidateOnReconnect?: boolean;
revalidateIfStale?: boolean;
}
const useFeature = ( const useFeature = (
projectId: string, projectId: string,
id: string, id: string,
options: IUseFeatureOptions = {} options: SWRConfiguration = {}
) => { ) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath( const path = formatApiPath(
@ -24,7 +17,9 @@ const useFeature = (
); );
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Feature toggle data')).then(res => res.json()); })
.then(handleErrorResponses('Feature toggle data'))
.then(res => res.json());
}; };
const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`; const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`;

View File

@ -1,24 +1,22 @@
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { IFeatureMetrics } from '../../../../interfaces/featureToggle'; import { IFeatureMetrics } from '../../../../interfaces/featureToggle';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureMetricsOptions {
refreshInterval?: number;
revalidateOnFocus?: boolean;
revalidateOnReconnect?: boolean;
revalidateIfStale?: boolean;
revalidateOnMount?: boolean;
}
const emptyMetrics = { lastHourUsage: [], seenApplications: [] }; const emptyMetrics = { lastHourUsage: [], seenApplications: [] };
const useFeatureMetrics = (projectId: string, featureId: string, options: IUseFeatureMetricsOptions = {}) => { const useFeatureMetrics = (
projectId: string,
featureId: string,
options: SWRConfiguration = {}
) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/client-metrics/features/${featureId}`); const path = formatApiPath(
`api/admin/client-metrics/features/${featureId}`
);
const res = await fetch(path, { const res = await fetch(path, {
method: 'GET' method: 'GET',
}).then(handleErrorResponses('feature metrics')); }).then(handleErrorResponses('feature metrics'));
if (res.ok) { if (res.ok) {
return res.json(); return res.json();
@ -32,7 +30,7 @@ const useFeatureMetrics = (projectId: string, featureId: string, options: IUseFe
FEATURE_METRICS_CACHE_KEY, FEATURE_METRICS_CACHE_KEY,
fetcher, fetcher,
{ {
...options ...options,
} }
); );
@ -51,7 +49,7 @@ const useFeatureMetrics = (projectId: string, featureId: string, options: IUseFe
error, error,
loading, loading,
refetch, refetch,
FEATURE_METRICS_CACHE_KEY FEATURE_METRICS_CACHE_KEY,
}; };
}; };

View File

@ -1,24 +1,16 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureStrategy } from '../../../../interfaces/strategy'; import { IFeatureStrategy } from '../../../../interfaces/strategy';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseFeatureOptions {
refreshInterval?: number;
revalidateOnFocus?: boolean;
revalidateOnReconnect?: boolean;
revalidateIfStale?: boolean;
revalidateOnMount?: boolean;
}
const useFeatureStrategy = ( const useFeatureStrategy = (
projectId: string, projectId: string,
featureId: string, featureId: string,
environmentId: string, environmentId: string,
strategyId: string, strategyId: string,
options: IUseFeatureOptions options: SWRConfiguration
) => { ) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath( const path = formatApiPath(
@ -26,7 +18,9 @@ const useFeatureStrategy = (
); );
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses(`Strategies for ${featureId}`)).then(res => res.json()); })
.then(handleErrorResponses(`Strategies for ${featureId}`))
.then(res => res.json());
}; };
const FEATURE_STRATEGY_CACHE_KEY = strategyId; const FEATURE_STRATEGY_CACHE_KEY = strategyId;

View File

@ -1,10 +1,10 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureType } from '../../../../interfaces/featureTypes'; import { IFeatureType } from '../../../../interfaces/featureTypes';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useFeatureTypes = () => { const useFeatureTypes = (options: SWRConfiguration = {}) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/feature-types`); const path = formatApiPath(`api/admin/feature-types`);
const res = await fetch(path, { const res = await fetch(path, {
@ -15,7 +15,7 @@ const useFeatureTypes = () => {
const KEY = `api/admin/feature-types`; const KEY = `api/admin/feature-types`;
const { data, error } = useSWR(KEY, fetcher); const { data, error } = useSWR(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { IProjectHealthReport } from '../../../../interfaces/project'; import { IProjectHealthReport } from '../../../../interfaces/project';
import { fallbackProject } from '../useProject/fallbackProject'; import { fallbackProject } from '../useProject/fallbackProject';
@ -6,19 +6,21 @@ import useSort from '../../../useSort';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useHealthReport = (id: string) => { const useHealthReport = (id: string, options: SWRConfiguration = {}) => {
const KEY = `api/admin/projects/${id}/health-report`; const KEY = `api/admin/projects/${id}/health-report`;
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/projects/${id}/health-report`); const path = formatApiPath(`api/admin/projects/${id}/health-report`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Health report')).then(res => res.json()); })
.then(handleErrorResponses('Health report'))
.then(res => res.json());
}; };
const [sort] = useSort(); const [sort] = useSort();
const { data, error } = useSWR<IProjectHealthReport>(KEY, fetcher); const { data, error } = useSWR<IProjectHealthReport>(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,15 +1,15 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getProjectFetcher } from './getProjectFetcher'; import { getProjectFetcher } from './getProjectFetcher';
import { IProject } from '../../../../interfaces/project'; import { IProject } from '../../../../interfaces/project';
import { fallbackProject } from './fallbackProject'; import { fallbackProject } from './fallbackProject';
import useSort from '../../../useSort'; import useSort from '../../../useSort';
const useProject = (id: string) => { const useProject = (id: string, options: SWRConfiguration = {}) => {
const { KEY, fetcher } = getProjectFetcher(id); const { KEY, fetcher } = getProjectFetcher(id);
const [sort] = useSort(); const [sort] = useSort();
const { data, error } = useSWR<IProject>(KEY, fetcher); const { data, error } = useSWR<IProject>(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,21 +1,27 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IProjectCard } from '../../../../interfaces/project'; import { IProjectCard } from '../../../../interfaces/project';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useProjects = () => { const useProjects = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/projects`); const path = formatApiPath(`api/admin/projects`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Projects')).then(res => res.json()); })
.then(handleErrorResponses('Projects'))
.then(res => res.json());
}; };
const KEY = `api/admin/projects`; const KEY = `api/admin/projects`;
const { data, error } = useSWR<{ projects: IProjectCard[] }>(KEY, fetcher); const { data, error } = useSWR<{ projects: IProjectCard[] }>(
KEY,
fetcher,
options
);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,4 +1,4 @@
import useSWR from 'swr'; import useSWR, { SWRConfiguration } from 'swr';
import useQueryParams from '../../../useQueryParams'; import useQueryParams from '../../../useQueryParams';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
@ -8,13 +8,15 @@ const getFetcher = (token: string) => () => {
const path = formatApiPath(`auth/reset/validate?token=${token}`); const path = formatApiPath(`auth/reset/validate?token=${token}`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Password reset')).then(res => res.json()); })
.then(handleErrorResponses('Password reset'))
.then(res => res.json());
}; };
const INVALID_TOKEN_ERROR = 'InvalidTokenError'; const INVALID_TOKEN_ERROR = 'InvalidTokenError';
const USED_TOKEN_ERROR = 'UsedTokenError'; const USED_TOKEN_ERROR = 'UsedTokenError';
const useResetPassword = () => { const useResetPassword = (options: SWRConfiguration = {}) => {
const query = useQueryParams(); const query = useQueryParams();
const initialToken = query.get('token') || ''; const initialToken = query.get('token') || '';
const [token, setToken] = useState(initialToken); const [token, setToken] = useState(initialToken);
@ -22,7 +24,7 @@ const useResetPassword = () => {
const fetcher = getFetcher(token); const fetcher = getFetcher(token);
const key = `auth/reset/validate?token=${token}`; const key = `auth/reset/validate?token=${token}`;
const { data, error } = useSWR(key, fetcher); const { data, error } = useSWR(key, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const retry = () => { const retry = () => {

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IStrategy } from '../../../../interfaces/strategy'; import { IStrategy } from '../../../../interfaces/strategy';
@ -11,30 +11,41 @@ const flexibleRolloutStrategy: IStrategy = {
name: 'flexibleRollout', name: 'flexibleRollout',
displayName: 'Gradual rollout', displayName: 'Gradual rollout',
editable: false, editable: false,
description: 'Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.', description:
parameters: [{ 'Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.',
name: 'rollout', type: 'percentage', description: '', required: false parameters: [
}, { {
name: 'stickiness', name: 'rollout',
type: 'string', type: 'percentage',
description: 'Used to defined stickiness', description: '',
required: true required: false,
}, { name: 'groupId', type: 'string', description: '', required: true }] },
{
name: 'stickiness',
type: 'string',
description: 'Used to defined stickiness',
required: true,
},
{ name: 'groupId', type: 'string', description: '', required: true },
],
}; };
const useStrategies = () => { const useStrategies = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/strategies`); const path = formatApiPath(`api/admin/strategies`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
credentials: 'include' credentials: 'include',
}).then(handleErrorResponses('Strategies')).then(res => res.json()); })
.then(handleErrorResponses('Strategies'))
.then(res => res.json());
}; };
const { data, error } = useSWR<{ strategies: IStrategy[] }>( const { data, error } = useSWR<{ strategies: IStrategy[] }>(
STRATEGIES_CACHE_KEY, STRATEGIES_CACHE_KEY,
fetcher fetcher,
options
); );
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
@ -50,7 +61,7 @@ const useStrategies = () => {
strategies: data?.strategies || [flexibleRolloutStrategy], strategies: data?.strategies || [flexibleRolloutStrategy],
error, error,
loading, loading,
refetch refetch,
}; };
}; };

View File

@ -1,10 +1,10 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { ITagType } from '../../../../interfaces/tags'; import { ITagType } from '../../../../interfaces/tags';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useTagTypes = () => { const useTagTypes = (options: SWRConfiguration = {}) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/tag-types`); const path = formatApiPath(`api/admin/tag-types`);
const res = await fetch(path, { const res = await fetch(path, {
@ -15,7 +15,7 @@ const useTagTypes = () => {
const KEY = `api/admin/tag-types`; const KEY = `api/admin/tag-types`;
const { data, error } = useSWR(KEY, fetcher); const { data, error } = useSWR(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,10 +1,10 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { ITag } from '../../../../interfaces/tags'; import { ITag } from '../../../../interfaces/tags';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useTags = (featureId: string) => { const useTags = (featureId: string, options: SWRConfiguration = {}) => {
const fetcher = async () => { const fetcher = async () => {
const path = formatApiPath(`api/admin/features/${featureId}/tags`); const path = formatApiPath(`api/admin/features/${featureId}/tags`);
const res = await fetch(path, { const res = await fetch(path, {
@ -15,7 +15,7 @@ const useTags = (featureId: string) => {
const KEY = `api/admin/features/${featureId}/tags`; const KEY = `api/admin/features/${featureId}/tags`;
const { data, error } = useSWR(KEY, fetcher); const { data, error } = useSWR(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { defaultValue } from './defaultValue'; import { defaultValue } from './defaultValue';
@ -7,17 +7,19 @@ import handleErrorResponses from '../httpErrorResponseHandler';
const REQUEST_KEY = 'api/admin/ui-config'; const REQUEST_KEY = 'api/admin/ui-config';
const useUiConfig = () => { const useUiConfig = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/ui-config`); const path = formatApiPath(`api/admin/ui-config`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
}).then(handleErrorResponses('configuration')).then(res => res.json()); })
.then(handleErrorResponses('configuration'))
.then(res => res.json());
}; };
const { data, error } = useSWR<IUiConfig>(REQUEST_KEY, fetcher); const { data, error } = useSWR<IUiConfig>(REQUEST_KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,23 +1,27 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useUnleashContext = (revalidate = true) => { const useUnleashContext = (
options: SWRConfiguration = {
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateIfStale: true,
}
) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/context`); const path = formatApiPath(`api/admin/context`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Context variables')).then(res => res.json()); })
.then(handleErrorResponses('Context variables'))
.then(res => res.json());
}; };
const CONTEXT_CACHE_KEY = 'api/admin/context'; const CONTEXT_CACHE_KEY = 'api/admin/context';
const { data, error } = useSWR(CONTEXT_CACHE_KEY, fetcher, { const { data, error } = useSWR(CONTEXT_CACHE_KEY, fetcher, options);
revalidateOnFocus: revalidate,
revalidateOnReconnect: revalidate,
revalidateIfStale: revalidate,
});
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);

View File

@ -1,4 +1,4 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import { IPermission } from '../../../../interfaces/user'; import { IPermission } from '../../../../interfaces/user';
@ -6,7 +6,7 @@ import handleErrorResponses from '../httpErrorResponseHandler';
export const USER_CACHE_KEY = `api/admin/user`; export const USER_CACHE_KEY = `api/admin/user`;
const useUser = () => { const useUser = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/user`); const path = formatApiPath(`api/admin/user`);
return fetch(path, { return fetch(path, {
@ -16,7 +16,7 @@ const useUser = () => {
.then(res => res.json()); .then(res => res.json());
}; };
const { data, error } = useSWR(USER_CACHE_KEY, fetcher); const { data, error } = useSWR(USER_CACHE_KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {

View File

@ -1,17 +1,19 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useUsers = () => { const useUsers = (options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/user-admin`); const path = formatApiPath(`api/admin/user-admin`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('Users')).then(res => res.json()); })
.then(handleErrorResponses('Users'))
.then(res => res.json());
}; };
const { data, error } = useSWR(`api/admin/user-admin`, fetcher); const { data, error } = useSWR(`api/admin/user-admin`, fetcher, options);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {