1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

Refactor: convert jsx files to typescript (#881)

* refactor: convert remaining js files to typescript

* refactor: conditionally render remove index

* refactor: dialog component to tsx

* refactor: migrate some files from jsx to tsx

* refactor: convert dropdown element to tsx

* refactor: feature toggle list to tsx

* refactor: update context name in use overrides

* refactor: variant overrides to tsx

refactor: remove unused strategy constraint file

* fix: tsx imports

* fix: update refectored components after rebase

* refactor: rename report list files to tsx

* fix: project health list types

* refactor: addon form - add types

* refactor: copy feature component types

* fix: projects toggle style after tsx refactor

* refactor: update ts types from openapi

* fix: ts refactor changes after review

* fix: header title prop

* fix: update after PR comments

* add test to useoverrides hook

* fix conditionally render time ago

* fix: toggle list empty tooltip

* fix: remove unused variable

* remove unused variable

* fix: remove faulty snapshot
This commit is contained in:
Tymoteusz Czech 2022-05-02 12:52:33 +02:00 committed by GitHub
parent 00341c6d67
commit 23a874d051
208 changed files with 1697 additions and 3398 deletions

View File

@ -1,4 +1,6 @@
import ConditionallyRender from 'component/common/ConditionallyRender';
import { createElement } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeedbackNPS } from 'component/feedback/FeedbackNPS/FeedbackNPS';
import { LayoutPicker } from 'component/layout/LayoutPicker/LayoutPicker';
import Loader from 'component/common/Loader/Loader';
@ -6,12 +8,12 @@ import NotFound from 'component/common/NotFound/NotFound';
import ProtectedRoute from 'component/common/ProtectedRoute/ProtectedRoute';
import SWRProvider from 'component/providers/SWRProvider/SWRProvider';
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
import styles from 'component/styles.module.scss';
import { Redirect, Route, Switch } from 'react-router-dom';
import { routes } from 'component/menu/routes';
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect';
import { IRoute } from 'interfaces/route';
import styles from 'component/styles.module.scss';
export const App = () => {
const { authDetails } = useAuthDetails();
@ -23,8 +25,7 @@ export const App = () => {
return !isLoggedIn;
};
// Change this to IRoute once snags with HashRouter and TS is worked out
const renderRoute = (route: any) => {
const renderRoute = (route: IRoute) => {
if (route.type === 'protected') {
const unauthorized = isUnauthorized();
return (
@ -40,13 +41,7 @@ export const App = () => {
<Route
key={route.path}
path={route.path}
render={props => (
<route.component
{...props}
isUnauthorized={isUnauthorized}
authDetails={authDetails}
/>
)}
render={() => createElement(route.component, {}, null)}
/>
);
};
@ -65,8 +60,9 @@ export const App = () => {
exact
path="/"
unauthorized={isUnauthorized()}
component={Redirect}
renderProps={{ to: '/features' }}
component={() => (
<Redirect to="/features" />
)}
/>
{routes.map(renderRoute)}
<Route path="/404" component={NotFound} />

View File

@ -2,7 +2,7 @@ import classnames from 'classnames';
import { Paper } from '@material-ui/core';
import CheckIcon from '@material-ui/icons/Check';
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import styles from './ReportCard.module.scss';
import ReactTimeAgo from 'react-timeago';
import { IProjectHealthReport } from 'interfaces/project';

View File

@ -1,25 +1,34 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, VFC } from 'react';
import { Paper, MenuItem } from '@material-ui/core';
import PropTypes from 'prop-types';
import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem';
import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFeaturesSort } from 'hooks/useFeaturesSort';
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import DropdownMenu from 'component/common/DropdownMenu/DropdownMenu';
import {
getObjectProperties,
getCheckedState,
applyCheckedToFeatures,
} from '../utils';
import { ReportToggleListItem } from './ReportToggleListItem/ReportToggleListItem';
import { ReportToggleListHeader } from './ReportToggleListHeader/ReportToggleListHeader';
import { useStyles } from './ReportToggleList.styles';
import { useFeaturesSort } from 'hooks/useFeaturesSort';
/* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */
const BULK_ACTIONS_ON = false;
const ReportToggleList = ({ features, selectedProject }) => {
interface IReportToggleListProps {
selectedProject: string;
features: IFeatureToggleListItem[];
}
export const ReportToggleList: VFC<IReportToggleListProps> = ({
features,
selectedProject,
}) => {
const styles = useStyles();
const [checkAll, setCheckAll] = useState(false);
const [localFeatures, setFeatures] = useState([]);
const [localFeatures, setFeatures] = useState<IFeatureToggleListItem[]>([]);
// @ts-expect-error
const { setSort, sorted } = useFeaturesSort(localFeatures);
useEffect(() => {
@ -32,10 +41,12 @@ const ReportToggleList = ({ features, selectedProject }) => {
'stale',
'type'
),
// @ts-expect-error
checked: getCheckedState(feature.name, features),
setFeatures,
}));
// @ts-expect-error
setFeatures(formattedFeatures);
}, [features, selectedProject]);
@ -50,6 +61,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
const renderListRows = () =>
sorted.map(feature => (
// @ts-expect-error
<ReportToggleListItem
key={feature.name}
{...feature}
@ -85,6 +97,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
<ReportToggleListHeader
handleCheckAll={handleCheckAll}
checkAll={checkAll}
// @ts-expect-error
setSort={setSort}
bulkActionsOn={BULK_ACTIONS_ON}
/>
@ -95,10 +108,3 @@ const ReportToggleList = ({ features, selectedProject }) => {
</Paper>
);
};
ReportToggleList.propTypes = {
selectedProject: PropTypes.string.isRequired,
features: PropTypes.array.isRequired,
};
export default ReportToggleList;

View File

@ -1,25 +1,27 @@
import React from 'react';
import { Dispatch, SetStateAction, VFC } from 'react';
import { Checkbox } from '@material-ui/core';
import UnfoldMoreOutlinedIcon from '@material-ui/icons/UnfoldMoreOutlined';
import PropTypes from 'prop-types';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import {
NAME,
LAST_SEEN,
CREATED,
EXPIRED,
STATUS,
} from 'component/Reporting/constants';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ReportingSortType } from 'component/Reporting/constants';
import { useStyles } from '../ReportToggleList.styles';
const ReportToggleListHeader = ({
interface IReportToggleListHeaderProps {
checkAll: boolean;
setSort: Dispatch<
SetStateAction<{ type: ReportingSortType; desc?: boolean }>
>;
bulkActionsOn: boolean;
handleCheckAll: () => void;
}
export const ReportToggleListHeader: VFC<IReportToggleListHeaderProps> = ({
handleCheckAll,
checkAll,
setSort,
bulkActionsOn,
}) => {
const styles = useStyles();
const handleSort = type => {
const handleSort = (type: ReportingSortType) => {
setSort(prev => ({
type,
desc: !prev.desc,
@ -47,7 +49,7 @@ const ReportToggleListHeader = ({
role="button"
tabIndex={0}
style={{ width: '150px' }}
onClick={() => handleSort(NAME)}
onClick={() => handleSort('name')}
>
Name
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -56,7 +58,7 @@ const ReportToggleListHeader = ({
role="button"
className={styles.hideColumnLastSeen}
tabIndex={0}
onClick={() => handleSort(LAST_SEEN)}
onClick={() => handleSort('last-seen')}
>
Last seen
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -65,7 +67,7 @@ const ReportToggleListHeader = ({
role="button"
tabIndex={0}
className={styles.hideColumn}
onClick={() => handleSort(CREATED)}
onClick={() => handleSort('created')}
>
Created
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -74,7 +76,7 @@ const ReportToggleListHeader = ({
role="button"
tabIndex={0}
className={styles.hideColumn}
onClick={() => handleSort(EXPIRED)}
onClick={() => handleSort('expired')}
>
Expired
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -83,7 +85,7 @@ const ReportToggleListHeader = ({
role="button"
tabIndex={0}
className={styles.hideColumnStatus}
onClick={() => handleSort(STATUS)}
onClick={() => handleSort('status')}
>
Status
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -91,7 +93,7 @@ const ReportToggleListHeader = ({
<th
role="button"
tabIndex={0}
onClick={() => handleSort(EXPIRED)}
onClick={() => handleSort('expired')}
>
Report
<UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -100,12 +102,3 @@ const ReportToggleListHeader = ({
</thead>
);
};
ReportToggleListHeader.propTypes = {
checkAll: PropTypes.bool.isRequired,
setSort: PropTypes.func.isRequired,
bulkActionsOn: PropTypes.bool.isRequired,
handleCheckAll: PropTypes.func.isRequired,
};
export default ReportToggleListHeader;

View File

@ -1,159 +0,0 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Checkbox } from '@material-ui/core';
import CheckIcon from '@material-ui/icons/Check';
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import FeatureStatus from 'component/feature/FeatureView/FeatureStatus/FeatureStatus';
import {
pluralize,
getDates,
expired,
toggleExpiryByTypeMap,
getDiffInDays,
} from 'component/Reporting/utils';
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
import { useStyles } from '../ReportToggleList.styles';
import { getTogglePath } from 'utils/routePathHelpers';
const ReportToggleListItem = ({
name,
stale,
lastSeenAt,
createdAt,
project,
type,
checked,
bulkActionsOn,
setFeatures,
}) => {
const styles = useStyles();
const nameMatches = feature => feature.name === name;
const handleChange = () => {
setFeatures(prevState => {
const newState = [...prevState];
return newState.map(feature => {
if (nameMatches(feature)) {
return { ...feature, checked: !feature.checked };
}
return feature;
});
});
};
const formatCreatedAt = () => {
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (diff === 0) return '1 day';
const formatted = pluralize(diff, 'day');
return `${formatted} ago`;
};
const formatExpiredAt = () => {
if (type === KILLSWITCH || type === PERMISSION) {
return 'N/A';
}
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (expired(diff, type)) {
const result = diff - toggleExpiryByTypeMap[type];
if (result === 0) return '1 day';
return pluralize(result, 'day');
}
return 'N/A';
};
const formatLastSeenAt = () => {
return (
<FeatureStatus lastSeenAt={lastSeenAt} tooltipPlacement="bottom" />
);
};
const renderStatus = (icon, text) => (
<span className={styles.reportStatus}>
{icon}
{text}
</span>
);
const formatReportStatus = () => {
if (type === KILLSWITCH || type === PERMISSION) {
return renderStatus(
<CheckIcon className={styles.reportIcon} />,
'Healthy'
);
}
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (expired(diff, type)) {
return renderStatus(
<ReportProblemOutlinedIcon className={styles.reportIcon} />,
'Potentially stale'
);
}
return renderStatus(
<CheckIcon className={styles.reportIcon} />,
'Healthy'
);
};
const statusClasses = classnames(styles.active, styles.hideColumnStatus, {
[styles.stale]: stale,
});
return (
<tr className={styles.tableRow}>
<ConditionallyRender
condition={bulkActionsOn}
show={
<td>
<Checkbox
checked={checked}
value={checked}
onChange={handleChange}
className={styles.checkbox}
/>
</td>
}
/>
<td>
<Link to={getTogglePath(project, name)} className={styles.link}>
{name}
</Link>
</td>
<td className={styles.hideColumnLastSeen}>{formatLastSeenAt()}</td>
<td className={styles.hideColumn}>{formatCreatedAt()}</td>
<td className={`${styles.expired} ${styles.hideColumn}`}>
{formatExpiredAt()}
</td>
<td className={statusClasses}>{stale ? 'Stale' : 'Active'}</td>
<td>{formatReportStatus()}</td>
</tr>
);
};
ReportToggleListItem.propTypes = {
name: PropTypes.string.isRequired,
stale: PropTypes.bool.isRequired,
lastSeenAt: PropTypes.string,
createdAt: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
bulkActionsOn: PropTypes.bool.isRequired,
setFeatures: PropTypes.func.isRequired,
};
export default React.memo(ReportToggleListItem);

View File

@ -0,0 +1,173 @@
import { memo, ReactNode } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { Checkbox } from '@material-ui/core';
import CheckIcon from '@material-ui/icons/Check';
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import FeatureStatus from 'component/feature/FeatureView/FeatureStatus/FeatureStatus';
import {
pluralize,
getDates,
expired,
toggleExpiryByTypeMap,
getDiffInDays,
} from 'component/Reporting/utils';
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
import { useStyles } from '../ReportToggleList.styles';
import { getTogglePath } from 'utils/routePathHelpers';
interface IReportToggleListItemProps {
name: string;
stale: boolean;
project: string;
lastSeenAt?: string;
createdAt: string;
type: string;
checked: boolean;
bulkActionsOn: boolean;
setFeatures: () => void;
}
export const ReportToggleListItem = memo<IReportToggleListItemProps>(
({
name,
stale,
lastSeenAt,
createdAt,
project,
type,
checked,
bulkActionsOn,
setFeatures,
}) => {
const styles = useStyles();
const nameMatches = (feature: { name: string }) =>
feature.name === name;
const handleChange = () => {
// @ts-expect-error
setFeatures(prevState => {
const newState = [...prevState];
return newState.map(feature => {
if (nameMatches(feature)) {
return { ...feature, checked: !feature.checked };
}
return feature;
});
});
};
const formatCreatedAt = () => {
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (diff === 0) return '1 day';
const formatted = pluralize(diff, 'day');
return `${formatted} ago`;
};
const formatExpiredAt = () => {
if (type === KILLSWITCH || type === PERMISSION) {
return 'N/A';
}
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (expired(diff, type)) {
const result = diff - toggleExpiryByTypeMap[type];
if (result === 0) return '1 day';
return pluralize(result, 'day');
}
return 'N/A';
};
const formatLastSeenAt = () => {
return (
<FeatureStatus
lastSeenAt={lastSeenAt}
tooltipPlacement="bottom"
/>
);
};
const renderStatus = (icon: ReactNode, text: ReactNode) => (
<span className={styles.reportStatus}>
{icon}
{text}
</span>
);
const formatReportStatus = () => {
if (type === KILLSWITCH || type === PERMISSION) {
return renderStatus(
<CheckIcon className={styles.reportIcon} />,
'Healthy'
);
}
const [date, now] = getDates(createdAt);
const diff = getDiffInDays(date, now);
if (expired(diff, type)) {
return renderStatus(
<ReportProblemOutlinedIcon className={styles.reportIcon} />,
'Potentially stale'
);
}
return renderStatus(
<CheckIcon className={styles.reportIcon} />,
'Healthy'
);
};
const statusClasses = classnames(
styles.active,
styles.hideColumnStatus,
{
[styles.stale]: stale,
}
);
return (
<tr className={styles.tableRow}>
<ConditionallyRender
condition={bulkActionsOn}
show={
<td>
<Checkbox
checked={checked}
value={checked}
onChange={handleChange}
className={styles.checkbox}
/>
</td>
}
/>
<td>
<Link
to={getTogglePath(project, name)}
className={styles.link}
>
{name}
</Link>
</td>
<td className={styles.hideColumnLastSeen}>
{formatLastSeenAt()}
</td>
<td className={styles.hideColumn}>{formatCreatedAt()}</td>
<td className={`${styles.expired} ${styles.hideColumn}`}>
{formatExpiredAt()}
</td>
<td className={statusClasses}>{stale ? 'Stale' : 'Active'}</td>
<td>{formatReportStatus()}</td>
</tr>
);
}
);

View File

@ -1,11 +1,7 @@
/* SORT TYPES */
export const NAME = 'name';
export const LAST_SEEN = 'last-seen';
export const CREATED = 'created';
export const EXPIRED = 'expired';
export const STATUS = 'status';
export const REPORT = 'report';
/* DAYS */
export const FOURTYDAYS = 40;
export const SEVENDAYS = 7;
export type ReportingSortType =
| 'name'
| 'last-seen'
| 'created'
| 'expired'
| 'status'
| 'report';

View File

@ -3,13 +3,15 @@ import differenceInDays from 'date-fns/differenceInDays';
import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes';
import { FOURTYDAYS, SEVENDAYS } from './constants';
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
const FORTY_DAYS = 40;
const SEVEN_DAYS = 7;
export const toggleExpiryByTypeMap: Record<string, number> = {
[EXPERIMENT]: FOURTYDAYS,
[RELEASE]: FOURTYDAYS,
[OPERATIONAL]: SEVENDAYS,
[EXPERIMENT]: FORTY_DAYS,
[RELEASE]: FORTY_DAYS,
[OPERATIONAL]: SEVEN_DAYS,
};
export interface IFeatureToggleListItemCheck extends IFeatureToggleListItem {

View File

@ -5,12 +5,12 @@ import { styles as commonStyles } from 'component/common';
import { IAddonProvider } from 'interfaces/addons';
interface IAddonProps {
provider: IAddonProvider;
provider?: IAddonProvider;
checkedEvents: string[];
setEventValue: (
name: string
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
error: Record<string, string>;
error?: string;
}
export const AddonEvents = ({

View File

@ -1,8 +1,17 @@
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
useState,
useEffect,
ChangeEvent,
VFC,
ChangeEventHandler,
FormEventHandler,
MouseEventHandler,
} from 'react';
import { TextField, FormControlLabel, Switch, Button } from '@material-ui/core';
import produce from 'immer';
import { styles as commonStyles } from 'component/common';
import { trim } from 'component/common/util';
import { IAddon, IAddonProvider } from 'interfaces/addons';
import { AddonParameters } from './AddonParameters/AddonParameters';
import { AddonEvents } from './AddonEvents/AddonEvents';
import cloneDeep from 'lodash.clonedeep';
@ -11,22 +20,48 @@ import { useHistory } from 'react-router-dom';
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
import useToast from 'hooks/useToast';
import { makeStyles } from '@material-ui/core/styles';
import { formatUnknownError } from 'utils/formatUnknownError';
const useStyles = makeStyles(theme => ({
nameInput: {
marginRight: '1.5rem',
},
formSection: { padding: '10px 28px' },
buttonsSection: {
padding: '10px 28px',
'& > *': {
marginRight: theme.spacing(1),
},
},
}));
export const AddonForm = ({ editMode, provider, addon, fetch }) => {
interface IAddonFormProps {
provider?: IAddonProvider;
addon: IAddon;
fetch: () => void;
editMode: boolean;
}
export const AddonForm: VFC<IAddonFormProps> = ({
editMode,
provider,
addon: initialValues,
fetch,
}) => {
const { createAddon, updateAddon } = useAddonsApi();
const { setToastData, setToastApiError } = useToast();
const history = useHistory();
const styles = useStyles();
const [config, setConfig] = useState(addon);
const [errors, setErrors] = useState({
const [formValues, setFormValues] = useState(initialValues);
const [errors, setErrors] = useState<{
containsErrors: boolean;
parameters: Record<string, string>;
events?: string;
general?: string;
description?: string;
}>({
containsErrors: false,
parameters: {},
});
const submitText = editMode ? 'Update' : 'Create';
@ -38,68 +73,73 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
}, [fetch, provider]); // empty array => fetch only first time
useEffect(() => {
setConfig({ ...addon });
setFormValues({ ...initialValues });
/* eslint-disable-next-line */
}, [addon.description, addon.provider]);
}, [initialValues.description, initialValues.provider]);
useEffect(() => {
if (provider && !config.provider) {
setConfig({ ...addon, provider: provider.name });
if (provider && !formValues.provider) {
setFormValues({ ...initialValues, provider: provider.name });
}
}, [provider, addon, config.provider]);
}, [provider, initialValues, formValues.provider]);
const setFieldValue = field => evt => {
evt.preventDefault();
const newConfig = { ...config };
newConfig[field] = evt.target.value;
setConfig(newConfig);
const setFieldValue =
(field: string): ChangeEventHandler<HTMLInputElement> =>
event => {
event.preventDefault();
setFormValues({ ...formValues, [field]: event.target.value });
};
const onEnabled: MouseEventHandler = event => {
event.preventDefault();
setFormValues(({ enabled }) => ({ ...formValues, enabled: !enabled }));
};
const onEnabled = evt => {
evt.preventDefault();
const enabled = !config.enabled;
setConfig({ ...config, enabled });
};
const setParameterValue =
(param: string): ChangeEventHandler<HTMLInputElement> =>
event => {
event.preventDefault();
setFormValues(
produce(draft => {
draft.parameters[param] = event.target.value;
})
);
};
const setParameterValue = param => evt => {
evt.preventDefault();
const newConfig = { ...config };
newConfig.parameters[param] = evt.target.value;
setConfig(newConfig);
};
const setEventValue = name => evt => {
const newConfig = { ...config };
if (evt.target.checked) {
newConfig.events.push(name);
} else {
newConfig.events = newConfig.events.filter(e => e !== name);
}
setConfig(newConfig);
setErrors({ ...errors, events: undefined });
};
const setEventValue =
(name: string) => (event: ChangeEvent<HTMLInputElement>) => {
const newConfig = { ...formValues };
if (event.target.checked) {
newConfig.events.push(name);
} else {
newConfig.events = newConfig.events.filter(e => e !== name);
}
setFormValues(newConfig);
setErrors({ ...errors, events: undefined });
};
const onCancel = () => {
history.goBack();
};
const onSubmit = async evt => {
evt.preventDefault();
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
event.preventDefault();
if (!provider) return;
const updatedErrors = cloneDeep(errors);
updatedErrors.containsErrors = false;
// Validations
if (config.events.length === 0) {
if (formValues.events.length === 0) {
updatedErrors.events = 'You must listen to at least one event';
updatedErrors.containsErrors = true;
}
provider.parameters.forEach(p => {
const value = trim(config.parameters[p.name]);
if (p.required && !value) {
updatedErrors.parameters[p.name] = 'This field is required';
provider.parameters.forEach(parameterConfig => {
const value = trim(formValues.parameters[parameterConfig.name]);
if (parameterConfig.required && !value) {
updatedErrors.parameters[parameterConfig.name] =
'This field is required';
updatedErrors.containsErrors = true;
}
});
@ -111,14 +151,14 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
try {
if (editMode) {
await updateAddon(config);
await updateAddon(formValues);
history.push('/addons');
setToastData({
type: 'success',
title: 'Addon updated successfully',
});
} else {
await createAddon(config);
await createAddon(formValues);
history.push('/addons');
setToastData({
type: 'success',
@ -126,11 +166,14 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
title: 'Addon created successfully',
});
}
} catch (e) {
setToastApiError({
text: e.toString(),
} catch (error) {
const message = formatUnknownError(error);
setToastApiError(message);
setErrors({
parameters: {},
general: message,
containsErrors: true,
});
setErrors({ parameters: {}, general: e.message });
}
};
@ -138,7 +181,7 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
name,
description,
documentationUrl = 'https://unleash.github.io/docs/addons',
} = provider ? provider : {};
} = provider ? provider : ({} as Partial<IAddonProvider>);
return (
<PageContent headerContent={`Configure ${name} addon`}>
@ -155,7 +198,7 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
size="small"
label="Provider"
name="provider"
value={config.provider}
value={formValues.provider}
disabled
variant="outlined"
className={styles.nameInput}
@ -163,24 +206,25 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
<FormControlLabel
control={
<Switch
checked={config.enabled}
onChange={onEnabled}
checked={formValues.enabled}
onClick={onEnabled}
/>
}
label={config.enabled ? 'Enabled' : 'Disabled'}
label={formValues.enabled ? 'Enabled' : 'Disabled'}
/>
</section>
<section className={styles.formSection}>
<TextField
size="small"
style={{ width: '80%' }}
rows={4}
minRows={4}
multiline
label="Description"
name="description"
placeholder=""
value={config.description}
error={errors.description}
value={formValues.description}
error={Boolean(errors.description)}
helperText={errors.description}
onChange={setFieldValue('description')}
variant="outlined"
/>
@ -188,7 +232,7 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
<section className={styles.formSection}>
<AddonEvents
provider={provider}
checkedEvents={config.events}
checkedEvents={formValues.events}
setEventValue={setEventValue}
error={errors.events}
/>
@ -196,13 +240,13 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
<section className={styles.formSection}>
<AddonParameters
provider={provider}
config={config}
errors={errors}
config={formValues}
parametersErrors={errors.parameters}
editMode={editMode}
setParameterValue={setParameterValue}
/>
</section>
<section className={styles.formSection}>
<section className={styles.buttonsSection}>
<Button type="submit" color="primary" variant="contained">
{submitText}
</Button>
@ -214,12 +258,3 @@ export const AddonForm = ({ editMode, provider, addon, fetch }) => {
</PageContent>
);
};
AddonForm.propTypes = {
provider: PropTypes.object,
addon: PropTypes.object.isRequired,
fetch: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
};

View File

@ -1,9 +1,6 @@
import { TextField } from '@material-ui/core';
import {
IAddonConfig,
IAddonProvider,
IAddonProviderParams,
} from 'interfaces/addons';
import { IAddonConfig, IAddonProviderParams } from 'interfaces/addons';
import { ChangeEventHandler } from 'react';
const resolveType = ({ type = 'text', sensitive = false }, value: string) => {
if (sensitive && value === MASKED_VALUE) {
@ -17,31 +14,29 @@ const resolveType = ({ type = 'text', sensitive = false }, value: string) => {
const MASKED_VALUE = '*****';
interface IAddonParameterProps {
provider: IAddonProvider;
errors: Record<string, string>;
export interface IAddonParameterProps {
parametersErrors: Record<string, string>;
definition: IAddonProviderParams;
setParameterValue: (param: string) => void;
setParameterValue: (param: string) => ChangeEventHandler<HTMLInputElement>;
config: IAddonConfig;
}
export const AddonParameter = ({
definition,
config,
errors,
parametersErrors,
setParameterValue,
}: IAddonParameterProps) => {
const value = config.parameters[definition.name] || '';
const type = resolveType(definition, value);
// @ts-expect-error
const error = errors.parameters[definition.name];
const error = parametersErrors[definition.name];
return (
<div style={{ width: '80%', marginTop: '25px' }}>
<TextField
size="small"
style={{ width: '100%' }}
rows={definition.type === 'textfield' ? 9 : 0}
minRows={definition.type === 'textfield' ? 9 : 0}
multiline={definition.type === 'textfield'}
type={type}
label={definition.displayName}
@ -51,8 +46,7 @@ export const AddonParameter = ({
shrink: true,
}}
value={value}
error={error}
// @ts-expect-error
error={Boolean(error)}
onChange={setParameterValue(definition.name)}
variant="outlined"
helperText={definition.description}

View File

@ -1,19 +1,22 @@
import React from 'react';
import { IAddonConfig, IAddonProvider } from 'interfaces/addons';
import { AddonParameter } from './AddonParameter/AddonParameter';
import { IAddonProvider } from 'interfaces/addons';
import {
AddonParameter,
IAddonParameterProps,
} from './AddonParameter/AddonParameter';
interface IAddonParametersProps {
provider: IAddonProvider;
errors: Record<string, string>;
provider?: IAddonProvider;
parametersErrors: IAddonParameterProps['parametersErrors'];
editMode: boolean;
setParameterValue: (param: string) => void;
config: IAddonConfig;
setParameterValue: IAddonParameterProps['setParameterValue'];
config: IAddonParameterProps['config'];
}
export const AddonParameters = ({
provider,
config,
errors,
parametersErrors,
setParameterValue,
editMode,
}: IAddonParametersProps) => {
@ -30,11 +33,10 @@ export const AddonParameters = ({
</p>
) : null}
{provider.parameters.map(parameter => (
// @ts-expect-error
<AddonParameter
key={parameter.name}
definition={parameter}
errors={errors}
parametersErrors={parametersErrors}
config={config}
setParameterValue={setParameterValue}
/>

View File

@ -3,7 +3,7 @@ import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
import { AvailableAddons } from './AvailableAddons/AvailableAddons';
import { Avatar } from '@material-ui/core';
import { DeviceHub } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import slackIcon from 'assets/icons/slack.svg';
import jiraIcon from 'assets/icons/jira.svg';
import webhooksIcon from 'assets/icons/webhooks.svg';

View File

@ -6,7 +6,7 @@ import {
ListItemText,
} from '@material-ui/core';
import { Delete, Edit, Visibility, VisibilityOff } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
DELETE_ADDON,
UPDATE_ADDON,
@ -20,7 +20,7 @@ import { ReactElement, useContext, useState } from 'react';
import AccessContext from 'contexts/AccessContext';
import { IAddon } from 'interfaces/addons';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { formatUnknownError } from 'utils/formatUnknownError';
interface IConfigureAddonsProps {

View File

@ -2,18 +2,19 @@ import { useParams } from 'react-router-dom';
import useAddons from 'hooks/api/getters/useAddons/useAddons';
import { AddonForm } from '../AddonForm/AddonForm';
import cloneDeep from 'lodash.clonedeep';
import { IAddon } from 'interfaces/addons';
interface IAddonCreateParams {
providerId: string;
}
const DEFAULT_DATA = {
export const DEFAULT_DATA = {
provider: '',
description: '',
enabled: true,
parameters: {},
events: [],
};
} as unknown as IAddon; // TODO: improve type
export const CreateAddon = () => {
const { providerId } = useParams<IAddonCreateParams>();
@ -31,7 +32,6 @@ export const CreateAddon = () => {
};
return (
// @ts-expect-error
<AddonForm
editMode={editMode}
provider={provider}

View File

@ -3,19 +3,12 @@ import useAddons from 'hooks/api/getters/useAddons/useAddons';
import { AddonForm } from '../AddonForm/AddonForm';
import cloneDeep from 'lodash.clonedeep';
import { IAddon } from 'interfaces/addons';
import { DEFAULT_DATA } from '../CreateAddon/CreateAddon';
interface IAddonEditParams {
addonId: string;
}
const DEFAULT_DATA = {
provider: '',
description: '',
enabled: true,
parameters: {},
events: [],
};
export const EditAddon = () => {
const { addonId } = useParams<IAddonEditParams>();
@ -26,12 +19,10 @@ export const EditAddon = () => {
(addon: IAddon) => addon.id === Number(addonId)
) || { ...cloneDeep(DEFAULT_DATA) };
const provider = addon
? // @ts-expect-error
providers.find(provider => provider.name === addon.provider)
? providers.find(provider => provider.name === addon.provider)
: undefined;
return (
// @ts-expect-error
<AddonForm
editMode={editMode}
provider={provider}

View File

@ -1,7 +1,7 @@
import AdminMenu from '../menu/AdminMenu';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ApiTokenPage } from 'component/admin/apiToken/ApiTokenPage/ApiTokenPage';
import { useLocation } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ApiTokenPage } from 'component/admin/apiToken/ApiTokenPage/ApiTokenPage';
import AdminMenu from '../menu/AdminMenu';
const ApiPage = () => {
const { pathname } = useLocation();

View File

@ -17,7 +17,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import { IAutocompleteBoxOption } from 'component/common/AutocompleteBox/AutocompleteBox';
import { useStyles } from '../ApiTokenForm.styles';
import { SelectAllButton } from './SelectAllButton/SelectAllButton';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const ALL_PROJECTS = '*';
@ -107,7 +107,7 @@ export const SelectProjectInput: VFC<ISelectProjectInputProps> = ({
const renderInput = (params: AutocompleteRenderInputParams) => (
<TextField
{...params}
error={!!error}
error={Boolean(error)}
helperText={error}
variant="outlined"
label="Projects"

View File

@ -1,5 +1,6 @@
import { useContext, useState } from 'react';
import {
Box,
IconButton,
Table,
TableBody,
@ -15,16 +16,15 @@ import useApiTokens from 'hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from 'component/common/ApiError/ApiError';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DELETE_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenList.styles';
import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import copy from 'copy-to-clipboard';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate';
import { ProjectsList } from './ProjectsList/ProjectsList';
import { useStyles } from './ApiTokenList.styles';
interface IApiToken {
createdAt: Date;
@ -182,7 +182,13 @@ export const ApiTokenList = () => {
}
/>
<TableCell className={styles.hideMD}>
<Secret value={item.secret} />
<Box
component="span"
display="inline-block"
width="250px"
>
************************************
</Box>
</TableCell>
<TableCell className={styles.actionsContainer}>
<Tooltip title="Copy token">

View File

@ -1,16 +0,0 @@
import PropTypes from 'prop-types';
function Secret({ value }) {
return (
<div>
<span style={{ width: '250px', display: 'inline-block' }}>
************************************
</span>
</div>
);
}
Secret.propTypes = {
value: PropTypes.string,
};
export default Secret;

View File

@ -4,8 +4,8 @@ import { Button } from '@material-ui/core';
import AccessContext from 'contexts/AccessContext';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import PageContent from 'component/common/PageContent';
import HeaderTitle from 'component/common/HeaderTitle';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
CREATE_API_TOKEN,
READ_API_TOKEN,

View File

@ -1,6 +1,6 @@
import { Typography } from '@material-ui/core';
import { useCommonStyles } from 'themes/commonStyles';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { UserToken } from './UserToken/UserToken';
interface IConfirmUserLink {

View File

@ -2,7 +2,7 @@ import React from 'react';
import AdminMenu from '../menu/AdminMenu';
import { Alert } from '@material-ui/lab';
import PageContent from 'component/common/PageContent/PageContent';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { OidcAuth } from './OidcAuth/OidcAuth';
import { SamlAuth } from './SamlAuth/SamlAuth';

View File

@ -104,7 +104,7 @@ export const AutoCreateForm = ({
label="Email domains"
name="emailDomains"
disabled={!data.autoCreate || !data.enabled}
required={!!data.autoCreate}
required={Boolean(data.autoCreate)}
value={data.emailDomains || ''}
placeholder="@company.com, @anotherCompany.com"
style={{ width: '400px' }}

View File

@ -2,7 +2,7 @@ import { useContext } from 'react';
import InvoiceList from './InvoiceList';
import AccessContext from 'contexts/AccessContext';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert } from '@material-ui/lab';
const InvoiceAdminPage = () => {

View File

@ -9,8 +9,8 @@ import {
} from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import PageContent from 'component/common/PageContent';
import HeaderTitle from 'component/common/HeaderTitle';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { formatApiPath } from 'utils/formatPath';
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
import { IInvoice } from 'interfaces/invoice';

View File

@ -9,7 +9,7 @@ import {
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { useStyles } from './ProjectRoleForm.styles';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import React, { ReactNode } from 'react';
import { IPermission } from 'interfaces/project';
import {

View File

@ -1,7 +1,7 @@
import { Alert } from '@material-ui/lab';
import React from 'react';
import { IProjectRole } from 'interfaces/role';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input';
import { useStyles } from './ProjectRoleDeleteConfirm.styles';

View File

@ -2,8 +2,8 @@ import { Button } from '@material-ui/core';
import { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import AccessContext from 'contexts/AccessContext';
import ConditionallyRender from 'component/common/ConditionallyRender';
import HeaderTitle from 'component/common/HeaderTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import PageContent from 'component/common/PageContent';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import AdminMenu from 'component/admin/menu/AdminMenu';

View File

@ -1,5 +1,5 @@
import { Typography } from '@material-ui/core';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ReactComponent as EmailIcon } from 'assets/icons/email.svg';
import { useStyles } from './ConfirmUserEmail.styles';

View File

@ -1,7 +1,7 @@
import { Typography } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { useCommonStyles } from 'themes/commonStyles';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import UserInviteLink from './UserInviteLink/UserInviteLink';
interface IConfirmUserLink {

View File

@ -11,7 +11,7 @@ import {
import { useStyles } from './UserForm.styles';
import React from 'react';
import useUsers from 'hooks/api/getters/useUsers/useUsers';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EDIT } from 'constants/misc';
import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap';

View File

@ -3,10 +3,10 @@ import UsersList from './UsersList/UsersList';
import AdminMenu from '../menu/AdminMenu';
import PageContent from 'component/common/PageContent/PageContent';
import AccessContext from 'contexts/AccessContext';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import HeaderTitle from 'component/common/HeaderTitle';
import { TableActions } from 'component/common/Table/TableActions/TableActions';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { Button } from '@material-ui/core';
import { useStyles } from './UserAdmin.styles';
import { useHistory } from 'react-router-dom';

View File

@ -3,11 +3,11 @@ import classnames from 'classnames';
import { Avatar, TextField, Typography } from '@material-ui/core';
import { trim } from 'component/common/util';
import { modalStyles } from 'component/admin/users/util';
import Dialogue from 'component/common/Dialogue/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PasswordChecker from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import { useCommonStyles } from 'themes/commonStyles';
import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert } from '@material-ui/lab';
import { IUser } from 'interfaces/user';

View File

@ -1,6 +1,6 @@
import React from 'react';
import Dialogue from 'component/common/Dialogue/Dialogue';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { REMOVE_USER_ERROR } from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import { Alert } from '@material-ui/lab';
import useLoading from 'hooks/useLoading';

View File

@ -10,7 +10,7 @@ import classnames from 'classnames';
import { Delete, Edit, Lock } from '@material-ui/icons';
import { SyntheticEvent, useContext } from 'react';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext';
import { IUser } from 'interfaces/user';
import { useHistory } from 'react-router-dom';

View File

@ -10,7 +10,7 @@ import {
import classnames from 'classnames';
import ChangePassword from './ChangePassword/ChangePassword';
import DeleteUser from './DeleteUser/DeleteUser';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';

View File

@ -9,13 +9,14 @@ import {
Typography,
} from '@material-ui/core';
import { Link as LinkIcon } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions';
import { ApplicationView } from '../ApplicationView/ApplicationView';
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
import Dialogue from 'component/common/Dialogue';
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PageContent from 'component/common/PageContent';
import HeaderTitle from 'component/common/HeaderTitle';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import AccessContext from 'contexts/AccessContext';
import useApplicationsApi from 'hooks/api/actions/useApplicationsApi/useApplicationsApi';
import useApplication from 'hooks/api/getters/useApplication/useApplication';
@ -25,7 +26,6 @@ import useToast from 'hooks/useToast';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { formatDateYMD } from 'utils/formatDate';
import { formatUnknownError } from 'utils/formatUnknownError';
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
export const ApplicationEdit = () => {
const history = useHistory();
@ -93,7 +93,7 @@ export const ApplicationEdit = () => {
<PageContent
headerContent={
<HeaderTitle
title={
titleElement={
<span
style={{
display: 'flex',
@ -106,6 +106,7 @@ export const ApplicationEdit = () => {
{appName}
</span>
}
title={appName}
actions={
<>
<ConditionallyRender

View File

@ -4,9 +4,9 @@ import { Warning } from '@material-ui/icons';
import { AppsLinkList, styles as commonStyles } from 'component/common';
import { SearchField } from 'component/common/SearchField/SearchField';
import PageContent from 'component/common/PageContent/PageContent';
import HeaderTitle from 'component/common/HeaderTitle';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import useApplications from 'hooks/api/getters/useApplications/useApplications';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
export const ApplicationList = () => {
const { applications, loading } = useApplications();

View File

@ -19,7 +19,7 @@ import {
CREATE_FEATURE,
CREATE_STRATEGY,
} from 'component/providers/AccessProvider/permissions';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { getTogglePath } from 'utils/routePathHelpers';
import useApplication from 'hooks/api/getters/useApplication/useApplication';
import AccessContext from 'contexts/AccessContext';

View File

@ -1,5 +1,5 @@
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
import FeatureToggleList from '../feature/FeatureToggleList/FeatureToggleList';
import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleList';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useFeaturesFilter } from 'hooks/useFeaturesFilter';
import { useFeatureArchiveApi } from 'hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi';
@ -20,7 +20,7 @@ export const ArchiveListContainer = () => {
const { filtered, filter, setFilter } = useFeaturesFilter(archivedFeatures);
const { sorted, sort, setSort } = useFeaturesSort(filtered);
const revive = (feature: string) => {
const onRevive = (feature: string) => {
reviveFeature(feature)
.then(refetchArchived)
.then(() =>
@ -38,13 +38,13 @@ export const ArchiveListContainer = () => {
<FeatureToggleList
features={sorted}
loading={loading}
revive={revive}
onRevive={onRevive}
flags={uiConfig.flags}
filter={filter}
setFilter={setFilter}
sort={sort}
setSort={setSort}
archive
isArchive
/>
);
};

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef, FC } from 'react';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IAnimateOnMountProps {
mounted: boolean;

View File

@ -1,6 +1,6 @@
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
import { Link, useLocation } from 'react-router-dom';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './BreadcrumbNav.styles';
import AccessContext from 'contexts/AccessContext';
import { useContext } from 'react';

View File

@ -15,7 +15,7 @@ type TargetElement =
type RenderFunc = () => JSX.Element;
const ConditionallyRender = ({
export const ConditionallyRender = ({
condition,
show,
elseShow,
@ -51,5 +51,3 @@ const ConditionallyRender = ({
}
return null;
};
export default ConditionallyRender;

View File

@ -1,3 +0,0 @@
import ConditionallyRender from './ConditionallyRender';
export default ConditionallyRender;

View File

@ -1,5 +1,5 @@
import { IConstraint } from 'interfaces/strategy';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConstraintAccordionEdit } from './ConstraintAccordionEdit/ConstraintAccordionEdit';
import { ConstraintAccordionView } from './ConstraintAccordionView/ConstraintAccordionView';

View File

@ -6,7 +6,7 @@ import { ConstraintFormHeader } from './ConstraintFormHeader/ConstraintFormHeade
import { useStyles } from './ConstraintAccordionEditBody.styles';
import React from 'react';
import { newOperators } from 'constants/operators';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { oneOf } from 'utils/oneOf';
import { OperatorUpgradeAlert } from 'component/common/OperatorUpgradeAlert/OperatorUpgradeAlert';

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Checkbox } from '@material-ui/core';
import { useCommonStyles } from 'themes/commonStyles';
import ConditionallyRender from 'component/common/ConditionallyRender';
import React, { useEffect, useState } from 'react';
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
import { ILegalValue } from 'interfaces/context';

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
import { FormControl, RadioGroup, Radio } from '@material-ui/core';
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useCommonStyles } from 'themes/commonStyles';
import { ILegalValue } from 'interfaces/context';
import {

View File

@ -5,7 +5,7 @@ import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashCon
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
import { Help } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { dateOperators, DATE_AFTER, IN } from 'constants/operators';
import { SAVE } from '../ConstraintAccordionEdit';
import { resolveText } from './helpers';

View File

@ -7,8 +7,8 @@ import { useWeakMap } from 'hooks/useWeakMap';
import { objectId } from 'utils/objectId';
import { useStyles } from './ConstraintAccordionList.styles';
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { Button } from '@material-ui/core';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IConstraintAccordionListProps {
constraints: IConstraint[];

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
import { stringOperators } from 'constants/operators';
import { IConstraint } from 'interfaces/strategy';
import { oneOf } from 'utils/oneOf';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
import { formatConstraintValue } from 'utils/formatConstraintValue';

View File

@ -5,7 +5,7 @@ import { Delete, Edit } from '@material-ui/icons';
import { IConstraint } from 'interfaces/strategy';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import React from 'react';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useLocationSettings } from 'hooks/useLocationSettings';

View File

@ -1,6 +1,6 @@
import { TextField, InputAdornment, Chip } from '@material-ui/core';
import { Search } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IConstraintValueSearchProps {
filter: string;

View File

@ -7,7 +7,7 @@ import {
DialogTitle,
} from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './Dialogue.styles';
import { DIALOGUE_CONFIRM_ID } from 'utils/testIds';
@ -15,7 +15,7 @@ interface IDialogue {
primaryButtonText?: string;
secondaryButtonText?: string;
open: boolean;
onClick: (e: React.SyntheticEvent) => void;
onClick?: (e: React.SyntheticEvent) => void;
onClose?: (e: React.SyntheticEvent) => void;
style?: object;
title: string;
@ -26,7 +26,7 @@ interface IDialogue {
permissionButton?: JSX.Element;
}
const Dialogue: React.FC<IDialogue> = ({
export const Dialogue: React.FC<IDialogue> = ({
children,
open,
onClick,
@ -44,7 +44,9 @@ const Dialogue: React.FC<IDialogue> = ({
const handleClick = formId
? (e: React.SyntheticEvent) => {
e.preventDefault();
onClick(e);
if (onClick) {
onClick(e);
}
}
: onClick;
return (
@ -103,5 +105,3 @@ const Dialogue: React.FC<IDialogue> = ({
</Dialog>
);
};
export default Dialogue;

View File

@ -1,3 +0,0 @@
import Dialogue from './Dialogue';
export default Dialogue;

View File

@ -0,0 +1,23 @@
import { ReactNode, VFC } from 'react';
import { Button, ButtonProps, Icon } from '@material-ui/core';
interface IDropdownButtonProps {
label: string;
id?: string;
title?: ButtonProps['title'];
className?: string;
icon?: ReactNode;
startIcon?: ButtonProps['startIcon'];
style?: ButtonProps['style'];
onClick: ButtonProps['onClick'];
}
export const DropdownButton: VFC<IDropdownButtonProps> = ({
label,
icon,
...rest
}) => (
<Button {...rest} endIcon={<Icon>{icon}</Icon>}>
{label}
</Button>
);

View File

@ -1,13 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CSSProperties,
MouseEventHandler,
ReactNode,
useState,
VFC,
} from 'react';
import { Menu } from '@material-ui/core';
import { ArrowDropDown } from '@material-ui/icons';
import { DropdownButton } from './DropdownButton/DropdownButton';
import { DropdownButton } from '..';
export interface IDropdownMenuProps {
renderOptions: () => ReactNode;
id: string;
title?: string;
callback?: MouseEventHandler;
icon?: ReactNode;
label: string;
startIcon?: ReactNode;
style?: CSSProperties;
}
import styles from '../common.module.scss';
const DropdownMenu = ({
const DropdownMenu: VFC<IDropdownMenuProps> = ({
renderOptions,
id,
title,
@ -18,11 +31,13 @@ const DropdownMenu = ({
startIcon,
...rest
}) => {
const [anchor, setAnchor] = React.useState(null);
const [anchor, setAnchor] = useState<Element | null>(null);
const handleOpen = e => setAnchor(e.currentTarget);
const handleOpen: MouseEventHandler<HTMLButtonElement> = e => {
setAnchor(e.currentTarget);
};
const handleClose = e => {
const handleClose: MouseEventHandler<HTMLDivElement> = e => {
if (callback && typeof callback === 'function') {
callback(e);
}
@ -46,7 +61,6 @@ const DropdownMenu = ({
/>
<Menu
id={id}
className={styles.dropdownMenu}
onClick={handleClose}
anchorEl={anchor}
open={Boolean(anchor)}
@ -57,14 +71,4 @@ const DropdownMenu = ({
);
};
DropdownMenu.propTypes = {
renderOptions: PropTypes.func,
id: PropTypes.string,
title: PropTypes.string,
callback: PropTypes.func,
icon: PropTypes.object,
label: PropTypes.string,
startIcon: PropTypes.object,
};
export default DropdownMenu;

View File

@ -1,6 +1,6 @@
import { useHistory } from 'react-router-dom';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import Dialogue from '../Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PermissionButton from '../PermissionButton/PermissionButton';
import { useStyles } from './EnvironmentStrategyDialog.styles';
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
@ -35,7 +35,6 @@ const EnvironmentStrategyDialog = ({
};
return (
// @ts-expect-error
<Dialogue
open={open}
maxWidth="sm"

View File

@ -8,7 +8,7 @@ import {
Tooltip,
} from '@material-ui/core';
import { FileCopy, Info } from '@material-ui/icons';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Loader from '../Loader/Loader';
import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';

View File

@ -1,15 +1,26 @@
import React from 'react';
import { ReactNode, VFC } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Typography } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
import { Variant } from '@material-ui/core/styles/createTypography';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './styles';
import { usePageTitle } from 'hooks/usePageTitle';
const HeaderTitle = ({
interface IHeaderTitleProps {
title: string;
titleElement?: ReactNode;
subtitle?: string;
variant?: 'inherit' | Variant;
loading?: boolean;
actions?: ReactNode;
className?: string;
}
export const HeaderTitle: VFC<IHeaderTitleProps> = ({
title,
titleElement,
actions,
subtitle,
variant,
@ -28,25 +39,15 @@ const HeaderTitle = ({
variant={variant || 'h1'}
className={classnames(styles.headerTitle, className)}
>
{title}
{titleElement || title}
</Typography>
{subtitle && <small>{subtitle}</small>}
</div>
<ConditionallyRender
condition={actions}
condition={Boolean(actions)}
show={<div className={styles.headerActions}>{actions}</div>}
/>
</div>
);
};
export default HeaderTitle;
HeaderTitle.propTypes = {
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
subtitle: PropTypes.string,
variant: PropTypes.string,
loading: PropTypes.bool,
actions: PropTypes.element,
};

View File

@ -1,3 +0,0 @@
import HeaderTitle from './HeaderTitle';
export default HeaderTitle;

View File

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TextField } from '@material-ui/core';
function InputListField({
label,
values = [],
error,
errorText,
name,
updateValues,
placeholder = '',
onBlur = () => {},
FormHelperTextProps,
}) {
const handleChange = evt => {
const values = evt.target.value.split(/,\s?/);
const trimmedValues = values.map(v => v.trim());
updateValues(trimmedValues);
};
const handleKeyDown = evt => {
if (evt.key === 'Backspace') {
const currentValue = evt.target.value;
if (currentValue.endsWith(', ')) {
evt.preventDefault();
const value = currentValue.slice(0, -2);
updateValues(value.split(/,\s*/));
}
}
};
return (
<TextField
name={name}
error={error}
helperText={errorText}
placeholder={placeholder}
value={values ? values.join(', ') : ''}
onKeyDown={handleKeyDown}
onChange={handleChange}
onBlur={onBlur}
label={label}
style={{ width: '100%' }}
variant="outlined"
size="small"
FormHelperTextProps={FormHelperTextProps}
/>
);
}
InputListField.propTypes = {
label: PropTypes.string.isRequired,
values: PropTypes.array,
error: PropTypes.string,
placeholder: PropTypes.string,
name: PropTypes.string.isRequired,
updateValues: PropTypes.func.isRequired,
onBlur: PropTypes.func,
};
export default InputListField;

View File

@ -0,0 +1,53 @@
import { VFC } from 'react';
import { TextField, TextFieldProps } from '@material-ui/core';
interface IInputListFieldProps {
label: string;
values?: any[];
error?: boolean;
placeholder?: string;
name: string;
updateValues: (values: string[]) => void;
onBlur?: TextFieldProps['onBlur'];
helperText?: TextFieldProps['helperText'];
FormHelperTextProps?: TextFieldProps['FormHelperTextProps'];
}
export const InputListField: VFC<IInputListFieldProps> = ({
values = [],
updateValues,
placeholder = '',
error,
...rest
}) => {
const handleChange: TextFieldProps['onChange'] = event => {
const values = event.target.value.split(/,\s?/);
const trimmedValues = values.map(v => v.trim());
updateValues(trimmedValues);
};
const handleKeyDown: TextFieldProps['onKeyDown'] = event => {
if (event.key === 'Backspace') {
const currentValue = (event.target as HTMLInputElement).value;
if (currentValue.endsWith(', ')) {
event.preventDefault();
const value = currentValue.slice(0, -2);
updateValues(value.split(/,\s*/));
}
}
};
return (
<TextField
{...rest}
error={error}
placeholder={placeholder}
value={values ? values.join(', ') : ''}
onKeyDown={handleKeyDown}
onChange={handleChange}
style={{ width: '100%' }}
variant="outlined"
size="small"
/>
);
};

View File

@ -1,6 +1,6 @@
import { ListItem } from '@material-ui/core';
import { Link } from 'react-router-dom';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from 'component/common/ListPlaceholder/ListPlaceholder.styles';
interface IListPlaceholderProps {

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import HeaderTitle from '../HeaderTitle';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { Paper } from '@material-ui/core';
import { useStyles } from './styles';

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import classnames from 'classnames';
import { useStyles } from './PaginationUI.styles';

View File

@ -1,8 +1,13 @@
import { IconButton, InputAdornment, TextField } from '@material-ui/core';
import {
IconButton,
InputAdornment,
TextField,
TextFieldProps,
} from '@material-ui/core';
import { Visibility, VisibilityOff } from '@material-ui/icons';
import React, { useState } from 'react';
import React, { useState, VFC } from 'react';
const PasswordField = ({ ...rest }) => {
const PasswordField: VFC<TextFieldProps> = ({ ...rest }) => {
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {

View File

@ -2,7 +2,7 @@ import { Button, ButtonProps } from '@material-ui/core';
import { Lock } from '@material-ui/icons';
import AccessContext from 'contexts/AccessContext';
import React, { useContext } from 'react';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
import { formatAccessText } from 'utils/formatAccessText';
import { useId } from 'hooks/useId';

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Alert } from '@material-ui/lab';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Typography } from '@material-ui/core';
import { useStyles } from './Proclamation.styles';
import { IProclamationToast } from 'interfaces/uiConfig';

View File

@ -1,79 +0,0 @@
import React from 'react';
import { MenuItem } from '@material-ui/core';
import PropTypes from 'prop-types';
import DropdownMenu from '../DropdownMenu/DropdownMenu';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
const ALL_PROJECTS = { id: '*', name: '> All projects' };
const ProjectSelect = ({ currentProjectId, updateCurrentProject, ...rest }) => {
const { projects } = useProjects();
const setProject = v => {
const id = v && typeof v === 'string' ? v.trim() : '*';
updateCurrentProject(id);
};
// TODO fixme
let curentProject = projects.find(i => i.id === currentProjectId);
if (!curentProject) {
curentProject = ALL_PROJECTS;
}
const handleChangeProject = e => {
const target = e.target.getAttribute('data-target');
setProject(target);
};
const renderProjectItem = (selectedId, item) => (
<MenuItem
disabled={selectedId === item.id}
data-target={item.id}
key={item.id}
style={{ fontSize: '14px' }}
>
{item.name}
</MenuItem>
);
const renderProjectOptions = () => {
const start = [
<MenuItem
disabled={curentProject === ALL_PROJECTS}
data-target={ALL_PROJECTS.id}
key={ALL_PROJECTS.id}
style={{ fontSize: '14px' }}
>
{ALL_PROJECTS.name}
</MenuItem>,
];
return [
...start,
...projects.map(p => renderProjectItem(currentProjectId, p)),
];
};
const { updateSetting, ...passDown } = rest;
return (
<React.Fragment>
<DropdownMenu
id={'project'}
title="Select project"
label={`${curentProject.name}`}
callback={handleChangeProject}
renderOptions={renderProjectOptions}
className=""
{...passDown}
/>
</React.Fragment>
);
};
ProjectSelect.propTypes = {
currentProjectId: PropTypes.string.isRequired,
updateCurrentProject: PropTypes.func.isRequired,
};
export default ProjectSelect;

View File

@ -0,0 +1,74 @@
import React, { MouseEventHandler, useMemo, VFC } from 'react';
import { MenuItem, Typography } from '@material-ui/core';
import DropdownMenu, { IDropdownMenuProps } from '../DropdownMenu/DropdownMenu';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { IProjectCard } from 'interfaces/project';
const ALL_PROJECTS = { id: '*', name: '> All projects' };
interface IProjectSelectProps {
currentProjectId: string;
updateCurrentProject: (id: string) => void;
}
const ProjectSelect: VFC<IProjectSelectProps & Partial<IDropdownMenuProps>> = ({
currentProjectId,
updateCurrentProject,
...rest
}) => {
const { projects } = useProjects();
const setProject = (value?: string | null) => {
const id = value && typeof value === 'string' ? value.trim() : '*';
updateCurrentProject(id);
};
const currentProject = useMemo(() => {
const project = projects.find(i => i.id === currentProjectId);
return project || ALL_PROJECTS;
}, [currentProjectId, projects]);
const handleChangeProject: MouseEventHandler = event => {
const target = (event.target as Element).getAttribute('data-target');
setProject(target);
};
const renderProjectItem = (selectedId: string, item: IProjectCard) => (
<MenuItem
disabled={selectedId === item.id}
data-target={item.id}
key={item.id}
style={{ fontSize: '14px' }}
>
{item.name}
</MenuItem>
);
const renderProjectOptions = () => [
<MenuItem
disabled={currentProject === ALL_PROJECTS}
data-target={ALL_PROJECTS.id}
key={ALL_PROJECTS.id}
>
<Typography variant="body2">{ALL_PROJECTS.name}</Typography>
</MenuItem>,
...projects.map(project =>
renderProjectItem(currentProjectId, project)
),
];
return (
<React.Fragment>
<DropdownMenu
id={'project'}
title="Select project"
label={`${currentProject.name}`}
callback={handleChangeProject}
renderOptions={renderProjectOptions}
{...rest}
/>
</React.Fragment>
);
};
export default ProjectSelect;

View File

@ -1,27 +0,0 @@
import { Route, useLocation, Redirect } from 'react-router-dom';
const ProtectedRoute = ({
component: Component,
unauthorized,
renderProps = {},
...rest
}) => {
const { pathname, search } = useLocation();
const redirect = encodeURIComponent(pathname + search);
const loginLink = `/login?redirect=${redirect}`;
return (
<Route
{...rest}
render={props => {
if (unauthorized) {
return <Redirect to={loginLink} />;
} else {
return <Component {...props} {...renderProps} />;
}
}}
/>
);
};
export default ProtectedRoute;

View File

@ -0,0 +1,24 @@
import { VFC } from 'react';
import { Route, useLocation, Redirect, RouteProps } from 'react-router-dom';
interface IProtectedRouteProps {
unauthorized?: boolean;
}
const ProtectedRoute: VFC<IProtectedRouteProps & RouteProps> = ({
component: Component,
unauthorized,
...rest
}) => {
const { pathname, search } = useLocation();
const redirect = encodeURIComponent(pathname + search);
const loginLink = `/login?redirect=${redirect}`;
return unauthorized ? (
<Route {...rest} render={() => <Redirect to={loginLink} />} />
) : (
<Route {...rest} component={Component} />
);
};
export default ProtectedRoute;

View File

@ -1,5 +1,5 @@
import { useMediaQuery } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionButton from '../PermissionButton/PermissionButton';
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
import React from 'react';

View File

@ -1,24 +1,24 @@
import React, { useState } from 'react';
import React, { useState, VFC } from 'react';
import classnames from 'classnames';
import { debounce } from 'debounce';
import { InputBase, Chip } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import { useStyles } from 'component/common/SearchField/styles';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface ISearchFieldProps {
updateValue: React.Dispatch<React.SetStateAction<string>>;
updateValue: (value: string) => void;
initialValue?: string;
className?: string;
showValueChip?: boolean;
}
export const SearchField = ({
export const SearchField: VFC<ISearchFieldProps> = ({
updateValue,
initialValue = '',
className = '',
showValueChip,
}: ISearchFieldProps) => {
}) => {
const styles = useStyles();
const [localValue, setLocalValue] = useState(initialValue);
const debounceUpdateValue = debounce(updateValue, 500);

View File

@ -1,3 +0,0 @@
import SearchField from './SearchField';
export default SearchField;

View File

@ -3,7 +3,7 @@ import { useStyles } from './StatusChip.styles';
interface IStatusChip {
stale: boolean;
showActive?: true;
showActive?: boolean;
}
const StatusChip = ({ stale, showActive = true }: IStatusChip) => {

View File

@ -1,5 +1,5 @@
import { Tooltip } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IStringTruncatorProps {
text: string;

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import { IconButton, Tooltip } from '@material-ui/core';
import { Search } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AnimateOnMount from 'component/common/AnimateOnMount/AnimateOnMount';
import { TableSearchField } from 'component/common/Table/TableActions/TableSearchField/TableSearchField';
import { useStyles } from 'component/common/Table/TableActions/TableActions.styles';

View File

@ -1,6 +1,6 @@
import { IconButton, InputBase, Tooltip } from '@material-ui/core';
import { Search, Close } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from 'component/common/Table/TableActions/TableSearchField/TableSearchField.styles';
import classnames from 'classnames';

View File

@ -7,7 +7,7 @@ import {
KeyboardArrowUp,
} from '@material-ui/icons';
import { IUsersSort, UsersSortType } from 'hooks/useUsersSort';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from 'component/common/Table/TableCellSortable/TableCellSortable.styles';
import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext';

View File

@ -4,7 +4,7 @@ import { useContext } from 'react';
import { IconButton, Tooltip } from '@material-ui/core';
import CheckMarkBadge from 'component/common/CheckmarkBadge/CheckMarkBadge';
import UIContext from 'contexts/UIContext';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Close from '@material-ui/icons/Close';
import { IToast } from 'interfaces/toast';

View File

@ -83,10 +83,6 @@
min-height: 200px;
}
.dropdownButton {
font-weight: normal;
}
.toggleName {
color: #37474f !important;
font-weight: 700;

View File

@ -15,7 +15,7 @@ import {
import { Apps } from '@material-ui/icons';
import styles from './common.module.scss';
import ConditionallyRender from './ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from './ConditionallyRender/ConditionallyRender';
export { styles };
@ -114,38 +114,6 @@ IconLink.propTypes = {
icon: PropTypes.object,
};
export const DropdownButton = ({
label,
id,
className = styles.dropdownButton,
title,
icon,
startIcon,
style,
...rest
}) => (
<Button
id={id}
className={className}
title={title}
style={style}
{...rest}
startIcon={startIcon}
endIcon={<Icon>{icon}</Icon>}
>
{label}
</Button>
);
DropdownButton.propTypes = {
label: PropTypes.string,
style: PropTypes.object,
id: PropTypes.string,
title: PropTypes.string,
icon: PropTypes.object,
startIcon: PropTypes.object,
};
export const MenuItemWithIcon = React.forwardRef(
({ icon: IconComponent, label, disabled, ...menuItemProps }, ref) => (
<MenuItem

View File

@ -1,6 +1,6 @@
import { useStyles } from 'component/context/ContectFormChip/ContextFormChip.styles';
import { Cancel } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IContextFormChipProps {
label: string;

View File

@ -1,11 +1,6 @@
import PageContent from 'component/common/PageContent/PageContent';
import HeaderTitle from 'component/common/HeaderTitle';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import {
CREATE_CONTEXT_FIELD,
DELETE_CONTEXT_FIELD,
UPDATE_CONTEXT_FIELD,
} from 'component/providers/AccessProvider/permissions';
import { useContext, useState, VFC } from 'react';
import { Add, Album, Delete, Edit } from '@material-ui/icons';
import { Link, useHistory } from 'react-router-dom';
import {
Button,
IconButton,
@ -16,30 +11,38 @@ import {
Tooltip,
useMediaQuery,
} from '@material-ui/core';
import { Add, Album, Delete, Edit } from '@material-ui/icons';
import { useContext, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { useStyles } from './styles';
import ConfirmDialogue from 'component/common/Dialogue';
import PageContent from 'component/common/PageContent/PageContent';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
CREATE_CONTEXT_FIELD,
DELETE_CONTEXT_FIELD,
UPDATE_CONTEXT_FIELD,
} from 'component/providers/AccessProvider/permissions';
import { Dialogue as ConfirmDialogue } from 'component/common/Dialogue/Dialogue';
import AccessContext from 'contexts/AccessContext';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useStyles } from './styles';
const ContextList = () => {
const ContextList: VFC = () => {
const { hasAccess } = useContext(AccessContext);
const [showDelDialogue, setShowDelDialogue] = useState(false);
const smallScreen = useMediaQuery('(max-width:700px)');
const [name, setName] = useState();
const [name, setName] = useState<string>();
const { context, refetchUnleashContext } = useUnleashContext();
const { removeContext } = useContextsApi();
const { setToastData, setToastApiError } = useToast();
const history = useHistory();
const styles = useStyles();
const onDeleteContext = async name => {
const onDeleteContext = async () => {
try {
if (name === undefined) {
throw new Error();
}
await removeContext(name);
refetchUnleashContext();
setToastData({
@ -93,6 +96,7 @@ const ContextList = () => {
show={
<Tooltip title="Delete context field">
<IconButton
aria-label="delete"
onClick={() => {
setName(field.name);
setShowDelDialogue(true);
@ -133,6 +137,7 @@ const ContextList = () => {
}
/>
);
return (
<PageContent
headerContent={
@ -151,7 +156,7 @@ const ContextList = () => {
</List>
<ConfirmDialogue
open={showDelDialogue}
onClick={() => onDeleteContext(name)}
onClick={onDeleteContext}
onClose={() => {
setName(undefined);
setShowDelDialogue(false);

View File

@ -10,10 +10,10 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PageContent from 'component/common/PageContent/PageContent';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import HeaderTitle from 'component/common/HeaderTitle/HeaderTitle';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import { formatUnknownError } from 'utils/formatUnknownError';
const CreateEnvironment = () => {

View File

@ -1,7 +1,7 @@
import { Alert } from '@material-ui/lab';
import React from 'react';
import { IEnvironment } from 'interfaces/environments';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input';
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
import { useStyles } from './EnvironmentDeleteConfirm.styles';

View File

@ -1,21 +1,21 @@
import HeaderTitle from 'component/common/HeaderTitle';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { Add } from '@material-ui/icons';
import PageContent from 'component/common/PageContent';
import { List } from '@material-ui/core';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { IEnvironment, ISortOrderPayload } from 'interfaces/environments';
import { useState } from 'react';
import { useHistory } from 'react-router-dom';
import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
import { List } from '@material-ui/core';
import { Add } from '@material-ui/icons';
import useToast from 'hooks/useToast';
import { IEnvironment, ISortOrderPayload } from 'interfaces/environments';
import { HeaderTitle } from 'component/common/HeaderTitle/HeaderTitle';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import PageContent from 'component/common/PageContent';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { formatUnknownError } from 'utils/formatUnknownError';
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
const EnvironmentList = () => {
const defaultEnv = {

View File

@ -12,7 +12,7 @@ import {
Edit,
OfflineBolt,
} from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IEnvironment } from 'interfaces/environments';
import React, { useContext, useRef } from 'react';

View File

@ -2,8 +2,8 @@ import { capitalize } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { IEnvironment } from 'interfaces/environments';
import ConditionallyRender from 'component/common/ConditionallyRender';
import Dialogue from 'component/common/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
interface IEnvironmentToggleConfirmProps {

View File

@ -1,4 +1,10 @@
import { useState, useRef, useEffect } from 'react';
import {
useState,
useRef,
useEffect,
FormEventHandler,
ChangeEventHandler,
} from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import {
Button,
@ -9,33 +15,35 @@ import {
} from '@material-ui/core';
import { FileCopy } from '@material-ui/icons';
import { styles as commonStyles } from 'component/common';
import { formatUnknownError } from 'utils/formatUnknownError';
import styles from './CopyFeature.module.scss';
import { trim } from 'component/common/util';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert } from '@material-ui/lab';
import { getTogglePath } from 'utils/routePathHelpers';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const CopyFeatureToggle = () => {
const [replaceGroupId, setReplaceGroupId] = useState(true);
const [apiError, setApiError] = useState('');
const [nameError, setNameError] = useState(undefined);
const [newToggleName, setNewToggleName] = useState();
const [nameError, setNameError] = useState<string | undefined>();
const [newToggleName, setNewToggleName] = useState<string>();
const { cloneFeatureToggle, validateFeatureToggleName } = useFeatureApi();
const inputRef = useRef();
const { name: copyToggleName, id: projectId } = useParams();
const inputRef = useRef<HTMLInputElement>();
const { name: copyToggleName, id: projectId } = useParams<{
name: string;
id: string;
}>();
const { feature } = useFeature(projectId, copyToggleName);
const { uiConfig } = useUiConfig();
const history = useHistory();
useEffect(() => {
inputRef.current?.focus();
}, []);
const setValue = evt => {
const value = trim(evt.target.value);
const setValue: ChangeEventHandler<HTMLInputElement> = event => {
const value = trim(event.target.value);
setNewToggleName(value);
};
@ -48,13 +56,13 @@ export const CopyFeatureToggle = () => {
await validateFeatureToggleName(newToggleName);
setNameError(undefined);
} catch (err) {
setNameError(err.message);
} catch (error) {
setNameError(formatUnknownError(error));
}
};
const onSubmit = async evt => {
evt.preventDefault();
const onSubmit: FormEventHandler = async event => {
event.preventDefault();
if (nameError) {
return;
@ -62,14 +70,12 @@ export const CopyFeatureToggle = () => {
try {
await cloneFeatureToggle(projectId, copyToggleName, {
name: newToggleName,
name: newToggleName as string,
replaceGroupId,
});
history.push(
getTogglePath(projectId, newToggleName, uiConfig.flags.E)
);
} catch (e) {
setApiError(e.toString());
history.push(getTogglePath(projectId, newToggleName as string));
} catch (error) {
setApiError(formatUnknownError(error));
}
};
@ -84,7 +90,7 @@ export const CopyFeatureToggle = () => {
<h1>Copy&nbsp;{copyToggleName}</h1>
</div>
<ConditionallyRender
condition={apiError}
condition={Boolean(apiError)}
show={<Alert severity="error">{apiError}</Alert>}
/>
<section className={styles.content}>
@ -116,7 +122,6 @@ export const CopyFeatureToggle = () => {
<Switch
value={replaceGroupId}
checked={replaceGroupId}
label="Replace groupId"
onChange={toggleReplaceGroupId}
/>
}

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Button, IconButton, Tooltip } from '@material-ui/core';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { Add } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { NAVIGATE_TO_CREATE_FEATURE } from 'utils/testIds';
import { IFeaturesFilter } from 'hooks/useFeaturesFilter';
import { useCreateFeaturePath } from 'component/feature/CreateFeatureButton/useCreateFeaturePath';

View File

@ -12,7 +12,7 @@ import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
import { projectFilterGenerator } from 'utils/projectFilterGenerator';
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { trim } from 'component/common/util';
import Input from 'component/common/Input/Input';
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert } from '@material-ui/lab';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';

View File

@ -13,7 +13,7 @@ import { useStyles } from './FeatureStrategyForm.styles';
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
import { useHistory } from 'react-router-dom';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import AccessContext from 'contexts/AccessContext';

View File

@ -1,4 +1,4 @@
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { Alert } from '@material-ui/lab';
import { Checkbox, FormControlLabel } from '@material-ui/core';
import { PRODUCTION } from 'constants/environmentTypes';

View File

@ -4,13 +4,13 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useHistory } from 'react-router-dom';
import useToast from 'hooks/useToast';
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
import Dialogue from 'component/common/Dialogue';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { Alert } from '@material-ui/lab';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { DELETE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { STRATEGY_FORM_REMOVE_ID } from 'utils/testIds';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Delete } from '@material-ui/icons';

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { ISegment } from 'interfaces/segment';
import { Clear, VisibilityOff, Visibility } from '@material-ui/icons';
import { useStyles } from './FeatureStrategySegmentChip.styles';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { Tooltip } from '@material-ui/core';

Some files were not shown because too many files have changed in this diff Show More