1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

refactor: port global settings to TS/hooks (#679)

* refactor: add ref support to PermissionSwitch

* refactor: port global settings to TS/hooks

* refactor: fix file extension

* refactor: format file

* refactor: fix inconsistent locationSettings prop

* refactor: use correct locationSettings hook

* refactor: use objects for settings hooks
This commit is contained in:
olav 2022-02-08 13:36:32 +01:00 committed by GitHub
parent 36f59b2290
commit fee1894c34
23 changed files with 217 additions and 230 deletions

View File

@ -31,6 +31,7 @@ import Dialogue from '../../../common/Dialogue';
import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
import { Alert } from '@material-ui/lab';
import copy from 'copy-to-clipboard';
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
interface IApiToken {
createdAt: Date;
@ -41,16 +42,13 @@ interface IApiToken {
environment: string;
}
interface IApiTokenList {
location: any;
}
const ApiTokenList = ({ location }: IApiTokenList) => {
const ApiTokenList = () => {
const styles = useStyles();
const { hasAccess } = useContext(AccessContext);
const { uiConfig } = useUiConfig();
const [showDelete, setShowDelete] = useState(false);
const [delToken, setDeleteToken] = useState<IApiToken>();
const { locationSettings } = useLocationSettings()
const { setToastData } = useToast();
const { tokens, loading, refetch, error } = useApiTokens();
const { deleteToken } = useApiTokensApi();
@ -150,7 +148,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
>
{formatDateWithLocale(
item.createdAt,
location.locale
locationSettings.locale
)}
</TableCell>
<TableCell

View File

@ -5,7 +5,7 @@ import AdminMenu from '../menu/AdminMenu';
import usePermissions from '../../../hooks/usePermissions';
import ConditionallyRender from '../../common/ConditionallyRender';
const ApiPage = ({ history, location }) => {
const ApiPage = ({ history }) => {
const { isAdmin } = usePermissions();
return (
@ -14,7 +14,7 @@ const ApiPage = ({ history, location }) => {
condition={isAdmin()}
show={<AdminMenu history={history} />}
/>
<ApiTokenList location={location} />
<ApiTokenList />
</div>
);
};
@ -22,7 +22,6 @@ const ApiPage = ({ history, location }) => {
ApiPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
};
export default ApiPage;

View File

@ -14,15 +14,15 @@ import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender';
import { formatApiPath } from '../../../utils/format-path';
import useInvoices from '../../../hooks/api/getters/useInvoices/useInvoices';
import { useLocation } from 'react-router-dom';
import { IInvoice } from '../../../interfaces/invoice';
import { useLocationSettings } from '../../../hooks/useLocationSettings';
const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
const InvoiceList = () => {
const { refetchInvoices, invoices } = useInvoices();
const [isLoaded, setLoaded] = useState(false);
const location = useLocation();
const { locationSettings } = useLocationSettings();
useEffect(() => {
refetchInvoices();
@ -89,7 +89,7 @@ const InvoiceList = () => {
{item.dueDate &&
formatDateWithLocale(
item.dueDate,
location.locale
locationSettings.locale
)}
</TableCell>
<TableCell

View File

@ -14,6 +14,7 @@ import AccessContext from '../../../../../contexts/AccessContext';
import { IUser } from '../../../../../interfaces/user';
import { useStyles } from './UserListItem.styles';
import { useHistory } from 'react-router-dom';
import { ILocationSettings } from "../../../../../hooks/useLocationSettings";
interface IUserListItemProps {
user: IUser;
@ -21,11 +22,7 @@ interface IUserListItemProps {
openUpdateDialog: (user: IUser) => (e: SyntheticEvent) => void;
openPwDialog: (user: IUser) => (e: SyntheticEvent) => void;
openDelDialog: (user: IUser) => (e: SyntheticEvent) => void;
location: ILocation;
}
interface ILocation {
locale: string;
locationSettings: ILocationSettings;
}
const UserListItem = ({
@ -34,7 +31,7 @@ const UserListItem = ({
openDelDialog,
openPwDialog,
openUpdateDialog,
location,
locationSettings,
}: IUserListItemProps) => {
const { hasAccess } = useContext(AccessContext);
const history = useHistory()
@ -54,7 +51,7 @@ const UserListItem = ({
</TableCell>
<TableCell>
<span data-loading>
{formatDateWithLocale(user.createdAt, location.locale)}
{formatDateWithLocale(user.createdAt, locationSettings.locale)}
</span>
</TableCell>
<TableCell className={styles.leftTableCell}>

View File

@ -20,10 +20,10 @@ import loadingData from './loadingData';
import useLoading from '../../../../hooks/useLoading';
import usePagination from '../../../../hooks/usePagination';
import PaginateUI from '../../../common/PaginateUI/PaginateUI';
import { useHistory } from 'react-router-dom';
import { IUser } from '../../../../interfaces/user';
import IRole from '../../../../interfaces/role';
import useToast from '../../../../hooks/useToast';
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
const UsersList = () => {
const { users, roles, refetch, loading } = useUsers();
@ -35,9 +35,8 @@ const UsersList = () => {
userLoading,
userApiErrors,
} = useAdminUsersApi();
const history = useHistory();
const { location } = history;
const { hasAccess } = useContext(AccessContext);
const { locationSettings } = useLocationSettings()
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
open: false,
});
@ -104,7 +103,7 @@ const UsersList = () => {
user={user}
openPwDialog={openPwDialog}
openDelDialog={openDelDialog}
location={location}
locationSettings={locationSettings}
renderRole={renderRole}
/>
));
@ -117,7 +116,7 @@ const UsersList = () => {
user={user}
openPwDialog={openPwDialog}
openDelDialog={openDelDialog}
location={location}
locationSettings={locationSettings}
renderRole={renderRole}
/>
);

View File

@ -19,6 +19,7 @@ test('renders correctly if no application', () => {
storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()}
history={{}}
locationSettings={{ locale: 'en-GB' }}
/>
</AccessProvider>
)
@ -77,7 +78,7 @@ test('renders correctly without permission', () => {
url: 'http://example.org',
description: 'app description',
}}
location={{ locale: 'en-GB' }}
locationSettings={{ locale: 'en-GB' }}
/>
</AccessProvider>
</ThemeProvider>
@ -140,7 +141,7 @@ test('renders correctly with permissions', () => {
url: 'http://example.org',
description: 'app description',
}}
location={{ locale: 'en-GB' }}
locationSettings={{ locale: 'en-GB' }}
/>
</AccessProvider>
</ThemeProvider>

View File

@ -33,7 +33,7 @@ class ClientApplications extends PureComponent {
fetchApplication: PropTypes.func.isRequired,
appName: PropTypes.string,
application: PropTypes.object,
location: PropTypes.object,
locationSettings: PropTypes.object.isRequired,
storeApplicationMetaData: PropTypes.func.isRequired,
deleteApplication: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
@ -54,8 +54,8 @@ class ClientApplications extends PureComponent {
.finally(() => this.setState({ loading: false }));
}
formatFullDateTime = v =>
formatFullDateTimeWithLocale(v, this.props.location.locale);
formatDate = v => formatDateWithLocale(v, this.props.location.locale);
formatFullDateTimeWithLocale(v, this.props.locationSettings.locale);
formatDate = v => formatDateWithLocale(v, this.props.locationSettings.locale);
deleteApplication = async evt => {
evt.preventDefault();

View File

@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import ApplicationEdit from './application-edit-component';
import { fetchApplication, storeApplicationMetaData, deleteApplication } from './../../store/application/actions';
const mapStateToProps = (state, props) => {
let application = state.applications.getIn(['apps', props.appName]);
const location = state.settings.toJS().location || {};
if (application) {
application = application.toJS();
}
return {
application,
location,
};
};
const Container = connect(mapStateToProps, {
fetchApplication,
storeApplicationMetaData,
deleteApplication,
})(ApplicationEdit);
export default Container;

View File

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import ApplicationEdit from './application-edit-component';
import {
deleteApplication,
fetchApplication,
storeApplicationMetaData,
} from '../../store/application/actions';
import { useLocationSettings } from '../../hooks/useLocationSettings';
const ApplicationEditContainer = props => {
const { locationSettings } = useLocationSettings();
return <ApplicationEdit {...props} locationSettings={locationSettings} />;
};
const mapStateToProps = (state, props) => {
let application = state.applications.getIn(['apps', props.appName]);
if (application) {
application = application.toJS();
}
return {
application,
};
};
export default connect(mapStateToProps, {
fetchApplication,
storeApplicationMetaData,
deleteApplication,
})(ApplicationEditContainer);

View File

@ -13,16 +13,21 @@ interface IPermissionSwitchProps extends OverridableComponent<any> {
checked: boolean;
}
const PermissionSwitch: React.FC<IPermissionSwitchProps> = ({
permission,
tooltip = '',
disabled,
projectId,
environmentId,
checked,
onChange,
...rest
}) => {
const PermissionSwitch = React.forwardRef<
HTMLButtonElement,
IPermissionSwitchProps
>((props, ref) => {
const {
permission,
tooltip = '',
disabled,
projectId,
environmentId,
checked,
onChange,
...rest
} = props;
const { hasAccess } = useContext(AccessContext);
let access;
@ -45,11 +50,12 @@ const PermissionSwitch: React.FC<IPermissionSwitchProps> = ({
onChange={onChange}
disabled={disabled || !access}
checked={checked}
ref={ref}
{...rest}
/>
</span>
</Tooltip>
);
};
});
export default PermissionSwitch;

View File

@ -1,25 +1,21 @@
import { Tooltip } from '@material-ui/core';
import { connect } from 'react-redux';
import { formatDateWithLocale, formatFullDateTimeWithLocale } from '../../../common/util';
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
interface CreatedAtProps {
time: Date;
//@ts-ignore
location: any;
}
const CreatedAt = ({time, location}: CreatedAtProps) => {
const CreatedAt = ({time}: CreatedAtProps) => {
const { locationSettings } = useLocationSettings();
return (
<Tooltip title={`Created at ${formatFullDateTimeWithLocale(time, location.locale)}`}>
<Tooltip title={`Created at ${formatFullDateTimeWithLocale(time, locationSettings.locale)}`}>
<span>
{formatDateWithLocale(time, location.locale)}
{formatDateWithLocale(time, locationSettings.locale)}
</span>
</Tooltip>
);
}
const mapStateToProps = (state: any) => ({
location: state.settings.toJS().location,
});
export default connect(mapStateToProps)(CreatedAt);
export default CreatedAt;

View File

@ -11,24 +11,21 @@ import EventCard from './EventCard/EventCard';
import { useStyles } from './EventLog.styles.js';
const EventLog = ({
updateSetting,
title,
history,
settings,
eventSettings,
setEventSettings,
locationSettings,
displayInline,
location,
hideName,
}) => {
const styles = useStyles();
const toggleShowDiff = () => {
updateSetting('showData', !settings.showData);
setEventSettings({ showData: !eventSettings.showData });
};
const formatFulldateTime = v => {
return formatFullDateTimeWithLocale(v, location.locale);
return formatFullDateTimeWithLocale(v, locationSettings.locale);
};
const showData = settings.showData;
if (!history || history.length < 0) {
return null;
}
@ -44,7 +41,7 @@ const EventLog = ({
</div>
);
if (showData) {
if (eventSettings.showData) {
entries = history.map(entry => (
<EventJson key={`log${entry.id}`} entry={entry} />
));
@ -63,7 +60,7 @@ const EventLog = ({
<FormControlLabel
control={
<Switch
checked={showData}
checked={eventSettings.showData}
onChange={toggleShowDiff}
color="primary"
/>
@ -82,12 +79,12 @@ const EventLog = ({
};
EventLog.propTypes = {
updateSettings: PropTypes.func,
history: PropTypes.array,
eventSettings: PropTypes.object.isRequired,
setEventSettings: PropTypes.func.isRequired,
locationSettings: PropTypes.object.isRequired,
title: PropTypes.string,
settings: PropTypes.object,
displayInline: PropTypes.bool,
location: PropTypes.object,
hideName: PropTypes.bool,
};
export default EventLog;

View File

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import EventLog from './EventLog';
import { updateSettingForGroup } from '../../../store/settings/actions';
const mapStateToProps = state => {
const settings = state.settings.toJS().history || {};
const location = state.settings.toJS().location || {};
return {
settings,
location,
};
};
const EventLogContainer = connect(mapStateToProps, {
updateSetting: updateSettingForGroup('history'),
})(EventLog);
export default EventLogContainer;

View File

@ -0,0 +1,27 @@
import EventLog from './EventLog';
import { useEventSettings } from "../../../hooks/useEventSettings";
import { useLocationSettings } from "../../../hooks/useLocationSettings";
interface IEventLogContainerProps {
title: string;
history: unknown[];
displayInline?: boolean;
}
const EventLogContainer = (props: IEventLogContainerProps) => {
const { locationSettings } = useLocationSettings();
const { eventSettings, setEventSettings } = useEventSettings();
return (
<EventLog
title={props.title}
history={props.history}
eventSettings={eventSettings}
setEventSettings={setEventSettings}
locationSettings={locationSettings}
displayInline={props.displayInline}
/>
);
};
export default EventLogContainer;

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import OutsideClickHandler from 'react-outside-click-handler';
import { Avatar, Button } from '@material-ui/core';
@ -8,17 +7,19 @@ import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import { useStyles } from './UserProfile.styles';
import { useCommonStyles } from '../../../common.styles';
import UserProfileContent from './UserProfileContent/UserProfileContent';
import { IUser } from '../../../interfaces/user';
import { IUser } from "../../../interfaces/user";
import { ILocationSettings } from "../../../hooks/useLocationSettings";
interface IUserProfileProps {
profile: IUser;
updateSettingLocation: (field: 'locale', value: string) => void;
profile: IUser
locationSettings: ILocationSettings
setLocationSettings: React.Dispatch<React.SetStateAction<ILocationSettings>>
}
const UserProfile = ({
profile,
location,
updateSettingLocation,
locationSettings,
setLocationSettings,
}: IUserProfileProps) => {
const [showProfile, setShowProfile] = useState(false);
const [currentLocale, setCurrentLocale] = useState<string>();
@ -40,17 +41,15 @@ const UserProfile = ({
]);
useEffect(() => {
const locale = location.locale || navigator.language;
let found = possibleLocales.find(l =>
l.toLowerCase().includes(locale.toLowerCase())
l.toLowerCase().includes(locationSettings.locale.toLowerCase())
);
setCurrentLocale(found);
if (!found) {
setPossibleLocales(prev => [...prev, locale]);
setPossibleLocales(prev => [...prev, locationSettings.locale]);
}
/* eslint-disable-next-line*/
}, []);
}, [locationSettings]);
const email = profile ? profile.email : '';
const imageUrl = email ? profile.imageUrl : 'unknown-user.png';
@ -75,7 +74,7 @@ const UserProfile = ({
showProfile={showProfile}
imageUrl={imageUrl}
profile={profile}
updateSettingLocation={updateSettingLocation}
setLocationSettings={setLocationSettings}
possibleLocales={possibleLocales}
setCurrentLocale={setCurrentLocale}
currentLocale={currentLocale}
@ -85,10 +84,4 @@ const UserProfile = ({
);
};
UserProfile.propTypes = {
profile: PropTypes.object,
location: PropTypes.object,
updateSettingLocation: PropTypes.func.isRequired,
};
export default UserProfile;

View File

@ -18,25 +18,28 @@ import legacyStyles from '../../user.module.scss';
import { getBasePath } from '../../../../utils/format-path';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import { IUser } from '../../../../interfaces/user';
import { ILocationSettings } from '../../../../hooks/useLocationSettings';
interface IUserProfileContentProps {
showProfile: boolean;
profile: IUser;
possibleLocales: string[];
updateSettingLocation: (field: 'locale', value: string) => void;
imageUrl: string;
currentLocale?: string;
setCurrentLocale: (value: string) => void;
setLocationSettings: React.Dispatch<
React.SetStateAction<ILocationSettings>
>;
}
const UserProfileContent = ({
showProfile,
profile,
possibleLocales,
updateSettingLocation,
imageUrl,
currentLocale,
setCurrentLocale,
setLocationSettings,
}: IUserProfileContentProps) => {
const commonStyles = useCommonStyles();
const { uiConfig } = useUiConfig();
@ -44,10 +47,6 @@ const UserProfileContent = ({
const [editingProfile, setEditingProfile] = useState(false);
const styles = useStyles();
const setLocale = (value: string) => {
updateSettingLocation('locale', value);
};
// @ts-expect-error
const profileAvatarClasses = classnames(styles.avatar, {
// @ts-expect-error
@ -61,9 +60,9 @@ const UserProfileContent = ({
});
const handleChange = (e: React.ChangeEvent<{ value: unknown }>) => {
const value = e.target.value as string;
setCurrentLocale(value);
setLocale(value);
const locale = e.target.value as string;
setCurrentLocale(locale);
setLocationSettings({ locale });
};
return (
@ -99,19 +98,14 @@ const UserProfileContent = ({
condition={!editingProfile}
show={
<>
<ConditionallyRender
condition={!uiConfig.disablePasswordAuth}
show={
<Button
variant="contained"
onClick={() =>
setEditingProfile(true)
}
>
Update password
</Button>
}
/>
<ConditionallyRender condition={!uiConfig.disablePasswordAuth} show={
<Button
variant="contained"
onClick={() => setEditingProfile(true)}
>
Update password
</Button>
} />
<div className={commonStyles.divider} />
<div className={legacyStyles.showUserSettings}>
<FormControl

View File

@ -1,27 +0,0 @@
import useUser from '../../../hooks/api/getters/useUser/useUser';
import { connect } from 'react-redux';
import UserProfile from './UserProfile';
import { updateSettingForGroup } from '../../../store/settings/actions';
const mapDispatchToProps = {
updateSettingLocation: updateSettingForGroup('location'),
};
const mapStateToProps = state => ({
location: state.settings ? state.settings.toJS().location : {},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(props => {
const user = useUser();
return (
<UserProfile
location={props.location}
updateSettingLocation={props.updateSettingLocation}
profile={user.user}
/>
);
});

View File

@ -0,0 +1,18 @@
import useUser from '../../../hooks/api/getters/useUser/useUser';
import UserProfile from './UserProfile';
import { useLocationSettings } from '../../../hooks/useLocationSettings';
const UserProfileContainer = () => {
const user = useUser();
const { locationSettings, setLocationSettings } = useLocationSettings();
return (
<UserProfile
locationSettings={locationSettings}
setLocationSettings={setLocationSettings}
profile={user.user}
/>
);
};
export default UserProfileContainer;

View File

@ -0,0 +1,27 @@
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import React from 'react';
export interface IEventSettings {
showData: boolean;
}
interface IUseEventSettingsOutput {
eventSettings: IEventSettings;
setEventSettings: React.Dispatch<React.SetStateAction<IEventSettings>>;
}
export const useEventSettings = (): IUseEventSettingsOutput => {
const [eventSettings, setEventSettings] = useGlobalState();
return { eventSettings, setEventSettings };
};
const createInitialValue = (): IEventSettings => {
return { showData: false };
};
const useGlobalState = createPersistentGlobalState<IEventSettings>(
`${getBasePath()}:useEventSettings:v1`,
createInitialValue()
);

View File

@ -0,0 +1,29 @@
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import React from 'react';
export interface ILocationSettings {
locale: string;
}
interface IUseLocationSettingsOutput {
locationSettings: ILocationSettings;
setLocationSettings: React.Dispatch<
React.SetStateAction<ILocationSettings>
>;
}
export const useLocationSettings = (): IUseLocationSettingsOutput => {
const [locationSettings, setLocationSettings] = useGlobalState();
return { locationSettings, setLocationSettings };
};
const createInitialValue = (): ILocationSettings => {
return { locale: navigator.language };
};
const useGlobalState = createPersistentGlobalState<ILocationSettings>(
`${getBasePath()}:useLocationSettings:v1`,
createInitialValue()
);

View File

@ -7,7 +7,6 @@ import tagTypes from './tag-type';
import tags from './tag';
import strategies from './strategy';
import error from './error';
import settings from './settings';
import user from './user';
import applications from './application';
import uiConfig from './ui-config';
@ -27,7 +26,6 @@ const unleashStore = combineReducers({
tags,
featureTags,
error,
settings,
user,
applications,
uiConfig,

View File

@ -1,10 +0,0 @@
export const UPDATE_SETTING = 'UPDATE_SETTING';
export const updateSetting = (group, field, value) => ({
type: UPDATE_SETTING,
group,
field,
value,
});
export const updateSettingForGroup = group => (field, value) => updateSetting(group, field, value);

View File

@ -1,44 +0,0 @@
import { fromJS } from 'immutable';
import { UPDATE_SETTING } from './actions';
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
import { getBasePath } from '../../utils/format-path';
const localStorage = window.localStorage || {
setItem: () => {},
getItem: () => {},
};
const basePath = getBasePath();
const SETTINGS = `${basePath}:settings`;
const DEFAULT = fromJS({ location: {} });
function getInitState() {
try {
const state = JSON.parse(localStorage.getItem(SETTINGS));
return state ? DEFAULT.merge(state) : DEFAULT;
} catch (e) {
return DEFAULT;
}
}
function updateSetting(state, action) {
const newState = state.updateIn([action.group, action.field], () => action.value);
localStorage.setItem(SETTINGS, JSON.stringify(newState.toJSON()));
return newState;
}
const settingStore = (state = getInitState(), action) => {
switch (action.type) {
case UPDATE_SETTING:
return updateSetting(state, action);
case USER_LOGOUT:
case USER_LOGIN:
return getInitState();
default:
return state;
}
};
export default settingStore;