mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
Merge branch 'master' into archive.display
This commit is contained in:
commit
09bd67f1f1
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
@ -29,6 +29,7 @@ build/Release
|
|||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules
|
node_modules
|
||||||
jspm_packages
|
jspm_packages
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
@ -40,3 +41,8 @@ typings/
|
|||||||
|
|
||||||
# Built
|
# Built
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
The latest version of this document is always available in
|
The latest version of this document is always available in
|
||||||
[releases][releases-url].
|
[releases][releases-url].
|
||||||
|
|
||||||
|
## [3.0.0-alpha.8]
|
||||||
|
- feat(timestamps): Make formatting of timestamps configurable.
|
||||||
|
- fix(package): Update react-mdl to version 1.11.0
|
||||||
|
- fix(package): update normalize.css to version 8.0.0
|
||||||
|
|
||||||
## [3.0.0-alpha.7]
|
## [3.0.0-alpha.7]
|
||||||
- Move metrics poller to seperate class
|
- Move metrics poller to seperate class
|
||||||
- Bugfix: CreatedAt set when creating new toggle
|
- Bugfix: CreatedAt set when creating new toggle
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-frontend",
|
"name": "unleash-frontend",
|
||||||
"description": "unleash your features",
|
"description": "unleash your features",
|
||||||
"version": "3.0.0-alpha.7",
|
"version": "3.0.0-alpha.8",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
@ -32,6 +32,7 @@
|
|||||||
"start": "NODE_ENV=development webpack-dev-server --progress --colors",
|
"start": "NODE_ENV=development webpack-dev-server --progress --colors",
|
||||||
"start:heroku": "UNLEASH_API=http://unleash.herokuapp.com npm run start",
|
"start:heroku": "UNLEASH_API=http://unleash.herokuapp.com npm run start",
|
||||||
"lint": "eslint . --ext js,jsx",
|
"lint": "eslint . --ext js,jsx",
|
||||||
|
"lint:fix": "eslint . --ext js,jsx --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:ci": "npm run lint && npm run build && npm run test",
|
"test:ci": "npm run lint && npm run build && npm run test",
|
||||||
"prepublish": "npm run build"
|
"prepublish": "npm run build"
|
||||||
@ -40,13 +41,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.1.0",
|
"debug": "^3.1.0",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"normalize.css": "^7.0.0",
|
"normalize.css": "^8.0.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^15.6.1",
|
"react": "^15.6.1",
|
||||||
"react-dnd": "^2.1.4",
|
"react-dnd": "^2.1.4",
|
||||||
"react-dnd-html5-backend": "^2.1.2",
|
"react-dnd-html5-backend": "^2.1.2",
|
||||||
"react-dom": "^15.6.1",
|
"react-dom": "^15.6.1",
|
||||||
"react-mdl": "^1.9.0",
|
"react-mdl": "^1.11.0",
|
||||||
"react-modal": "^1.6.4",
|
"react-modal": "^1.6.4",
|
||||||
"react-redux": "^4.4.5",
|
"react-redux": "^4.4.5",
|
||||||
"react-router": "^3.0.0",
|
"react-router": "^3.0.0",
|
||||||
|
BIN
frontend/public/en-GB.png
Normal file
BIN
frontend/public/en-GB.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
frontend/public/nb-NO.png
Normal file
BIN
frontend/public/nb-NO.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/public/unknown-locale.png
Normal file
BIN
frontend/public/unknown-locale.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/unknown-user.png
Normal file
BIN
frontend/public/unknown-user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/public/us-US.png
Normal file
BIN
frontend/public/us-US.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -21,7 +21,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from 'react-mdl';
|
} from 'react-mdl';
|
||||||
import { IconLink, shorten, styles as commonStyles } from '../common';
|
import { IconLink, shorten, styles as commonStyles } from '../common';
|
||||||
import { formatFullDateTime } from '../common/util';
|
import { formatFullDateTimeWithLocale } from '../common/util';
|
||||||
|
|
||||||
class StatefulTextfield extends Component {
|
class StatefulTextfield extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -59,6 +59,7 @@ class ClientApplications extends PureComponent {
|
|||||||
fetchApplication: PropTypes.func.isRequired,
|
fetchApplication: PropTypes.func.isRequired,
|
||||||
appName: PropTypes.string,
|
appName: PropTypes.string,
|
||||||
application: PropTypes.object,
|
application: PropTypes.object,
|
||||||
|
location: PropTypes.object,
|
||||||
storeApplicationMetaData: PropTypes.func.isRequired,
|
storeApplicationMetaData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,7 +71,9 @@ class ClientApplications extends PureComponent {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchApplication(this.props.appName);
|
this.props.fetchApplication(this.props.appName);
|
||||||
}
|
}
|
||||||
|
formatFullDateTime(v) {
|
||||||
|
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.application) {
|
if (!this.props.application) {
|
||||||
return <ProgressBar indeterminate />;
|
return <ProgressBar indeterminate />;
|
||||||
@ -142,7 +145,8 @@ class ClientApplications extends PureComponent {
|
|||||||
icon="timeline"
|
icon="timeline"
|
||||||
subtitle={
|
subtitle={
|
||||||
<span>
|
<span>
|
||||||
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
|
{clientIp} last seen at{' '}
|
||||||
|
<small>{this.formatFullDateTime(lastSeen)}</small>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -4,11 +4,13 @@ import { fetchApplication, storeApplicationMetaData } from '../../store/applicat
|
|||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
let application = state.applications.getIn(['apps', props.appName]);
|
let application = state.applications.getIn(['apps', props.appName]);
|
||||||
|
const location = state.settings.toJS().location || {};
|
||||||
if (application) {
|
if (application) {
|
||||||
application = application.toJS();
|
application = application.toJS();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
application,
|
application,
|
||||||
|
location,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import { formatFullDateTime } from '../util';
|
import { formatFullDateTimeWithLocale } from '../util';
|
||||||
|
|
||||||
test('formats dates correctly', () => {
|
test('formats dates correctly', () => {
|
||||||
expect(formatFullDateTime(1487861809466)).toEqual('2017-02-23 14:56:49');
|
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'UTC')).toEqual('2017-02-23 14:56:49');
|
||||||
expect(formatFullDateTime(1487232809466)).toEqual('2017-02-16 08:13:29');
|
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Paris')).toEqual('2017-02-23 15:56:49');
|
||||||
expect(formatFullDateTime(1477232809466)).toEqual('2016-10-23 14:26:49');
|
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/Oslo')).toEqual('2017-02-23 15:56:49');
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO', 'Europe/London')).toEqual('2017-02-23 14:56:49');
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Paris')).toEqual('02/23/2017, 3:56:49 PM');
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/Oslo')).toEqual('02/23/2017, 3:56:49 PM');
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB', 'Europe/London')).toEqual('02/23/2017, 2:56:49 PM');
|
||||||
|
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'nb-NO')).toEqual(
|
||||||
|
expect.stringMatching(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/)
|
||||||
|
);
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'en-GB')).toEqual(expect.stringContaining('02/23/2017'));
|
||||||
|
expect(formatFullDateTimeWithLocale(1487861809466, 'en-US')).toEqual(expect.stringContaining('02/23/2017'));
|
||||||
});
|
});
|
||||||
|
@ -6,5 +6,9 @@ const dateTimeOptions = {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
};
|
};
|
||||||
|
export const formatFullDateTimeWithLocale = (v, locale, tz) => {
|
||||||
export const formatFullDateTime = v => new Date(v).toLocaleString('nb-NO', dateTimeOptions);
|
if (tz) {
|
||||||
|
dateTimeOptions.timeZone = tz;
|
||||||
|
}
|
||||||
|
return new Date(v).toLocaleString(locale, dateTimeOptions);
|
||||||
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { Grid, Cell, Icon, Chip, ChipContact } from 'react-mdl';
|
|||||||
import Progress from './progress';
|
import Progress from './progress';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { AppsLinkList, calc } from '../common';
|
import { AppsLinkList, calc } from '../common';
|
||||||
import { formatFullDateTime } from '../common/util';
|
import { formatFullDateTimeWithLocale } from '../common/util';
|
||||||
import styles from './metrics.scss';
|
import styles from './metrics.scss';
|
||||||
|
|
||||||
const StrategyChipItem = ({ strategy }) => (
|
const StrategyChipItem = ({ strategy }) => (
|
||||||
@ -38,13 +38,16 @@ export default class MetricComponent extends React.Component {
|
|||||||
featureToggle: PropTypes.object.isRequired,
|
featureToggle: PropTypes.object.isRequired,
|
||||||
fetchSeenApps: PropTypes.func.isRequired,
|
fetchSeenApps: PropTypes.func.isRequired,
|
||||||
fetchFeatureMetrics: PropTypes.func.isRequired,
|
fetchFeatureMetrics: PropTypes.func.isRequired,
|
||||||
|
location: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.props.fetchSeenApps();
|
this.props.fetchSeenApps();
|
||||||
this.props.fetchFeatureMetrics();
|
this.props.fetchFeatureMetrics();
|
||||||
}
|
}
|
||||||
|
formatFullDateTime(v) {
|
||||||
|
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
const { metrics = {}, featureToggle } = this.props;
|
const { metrics = {}, featureToggle } = this.props;
|
||||||
const {
|
const {
|
||||||
@ -107,7 +110,7 @@ export default class MetricComponent extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AppsLinkList apps={seenApps} />
|
<AppsLinkList apps={seenApps} />
|
||||||
<span>Created {formatFullDateTime(featureToggle.createdAt)}</span>
|
<span>Created {this.formatFullDateTime(featureToggle.createdAt)}</span>
|
||||||
</Cell>
|
</Cell>
|
||||||
</Grid>
|
</Grid>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -23,6 +23,7 @@ function getMetricsForToggle(state, toggleName) {
|
|||||||
export default connect(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
metrics: getMetricsForToggle(state, props.featureToggle.name),
|
metrics: getMetricsForToggle(state, props.featureToggle.name),
|
||||||
|
location: state.settings.toJS().location || {},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchFeatureMetrics,
|
fetchFeatureMetrics,
|
||||||
|
@ -4,7 +4,7 @@ import HistoryItemDiff from './history-item-diff';
|
|||||||
import HistoryItemJson from './history-item-json';
|
import HistoryItemJson from './history-item-json';
|
||||||
import { Table, TableHeader } from 'react-mdl';
|
import { Table, TableHeader } from 'react-mdl';
|
||||||
import { DataTableHeader, SwitchWithLabel, styles as commonStyles } from '../common';
|
import { DataTableHeader, SwitchWithLabel, styles as commonStyles } from '../common';
|
||||||
import { formatFullDateTime } from '../common/util';
|
import { formatFullDateTimeWithLocale } from '../common/util';
|
||||||
|
|
||||||
import styles from './history.scss';
|
import styles from './history.scss';
|
||||||
|
|
||||||
@ -13,13 +13,16 @@ class HistoryList extends Component {
|
|||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
history: PropTypes.array,
|
history: PropTypes.array,
|
||||||
settings: PropTypes.object,
|
settings: PropTypes.object,
|
||||||
|
location: PropTypes.object,
|
||||||
updateSetting: PropTypes.func.isRequired,
|
updateSetting: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleShowDiff() {
|
toggleShowDiff() {
|
||||||
this.props.updateSetting('showData', !this.props.settings.showData);
|
this.props.updateSetting('showData', !this.props.settings.showData);
|
||||||
}
|
}
|
||||||
|
formatFulldateTime(v) {
|
||||||
|
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
const showData = this.props.settings.showData;
|
const showData = this.props.settings.showData;
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
@ -62,7 +65,12 @@ class HistoryList extends Component {
|
|||||||
User
|
User
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableHeader name="diff">Diff</TableHeader>
|
<TableHeader name="diff">Diff</TableHeader>
|
||||||
<TableHeader numeric name="createdAt" cellFormatter={formatFullDateTime} style={{ width: '165px' }}>
|
<TableHeader
|
||||||
|
numeric
|
||||||
|
name="createdAt"
|
||||||
|
cellFormatter={this.formatFulldateTime.bind(this)}
|
||||||
|
style={{ width: '165px' }}
|
||||||
|
>
|
||||||
Time
|
Time
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
</Table>
|
</Table>
|
||||||
|
@ -4,9 +4,10 @@ import { updateSettingForGroup } from '../../store/settings/actions';
|
|||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const settings = state.settings.toJS().history || {};
|
const settings = state.settings.toJS().history || {};
|
||||||
|
const location = state.settings.toJS().location || {};
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
|
location,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,19 +5,51 @@ import styles from './user.scss';
|
|||||||
export default class ShowUserComponent extends React.Component {
|
export default class ShowUserComponent extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
profile: PropTypes.object,
|
profile: PropTypes.object,
|
||||||
|
location: PropTypes.object,
|
||||||
fetchUser: PropTypes.func.isRequired,
|
fetchUser: PropTypes.func.isRequired,
|
||||||
|
updateSettingLocation: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
possibleLocales = [
|
||||||
|
{ value: 'nb-NO', image: 'nb-NO' },
|
||||||
|
{ value: 'us-US', image: 'us-US' },
|
||||||
|
{ value: 'en-GB', image: 'en-GB' },
|
||||||
|
];
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUser();
|
this.props.fetchUser();
|
||||||
|
// find default locale and add it in choices if not present
|
||||||
|
let locale = navigator.language;
|
||||||
|
let found = this.possibleLocales.find(l => l.value === locale);
|
||||||
|
if (!found) {
|
||||||
|
this.possibleLocales.push({ value: locale, image: 'unknown-locale' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocale() {
|
||||||
|
const locale = this.props.location
|
||||||
|
? this.props.location.locale
|
||||||
|
: this.possibleLocales[this.possibleLocales.length - 1];
|
||||||
|
let index = this.possibleLocales.findIndex(v => v.value === locale);
|
||||||
|
index = (index + 1) % this.possibleLocales.length;
|
||||||
|
this.props.updateSettingLocation('locale', this.possibleLocales[index].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const email = this.props.profile ? this.props.profile.email : '';
|
const email = this.props.profile ? this.props.profile.email : '';
|
||||||
const imageUrl = email ? this.props.profile.imageUrl : '';
|
const locale = this.props.location
|
||||||
|
? this.props.location.locale
|
||||||
|
: this.possibleLocales[this.possibleLocales.length - 1].value;
|
||||||
|
let foundLocale = this.possibleLocales.find(l => l.value === locale);
|
||||||
|
const imageUrl = email ? this.props.profile.imageUrl : 'public/unknown-user.png';
|
||||||
|
const imageLocale = foundLocale ? `public/${foundLocale.image}.png` : `public/unknown-locale.png`;
|
||||||
return (
|
return (
|
||||||
<div className={styles.showUser}>
|
<div className={styles.showUserSettings}>
|
||||||
<img src={imageUrl} title={email} alt={email} />
|
<div className={styles.showLocale}>
|
||||||
|
<img src={imageLocale} title={locale} alt={locale} onClick={this.updateLocale.bind(this)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.showUser}>
|
||||||
|
<img src={imageUrl} title={email} alt={email} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ShowUserComponent from './show-user-component';
|
import ShowUserComponent from './show-user-component';
|
||||||
import { fetchUser } from '../../store/user/actions';
|
import { fetchUser } from '../../store/user/actions';
|
||||||
|
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchUser,
|
fetchUser,
|
||||||
|
updateSettingLocation: updateSettingForGroup('location'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
profile: state.user.get('profile'),
|
profile: state.user.get('profile'),
|
||||||
|
location: state.settings ? state.settings.toJS().location : {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent);
|
||||||
|
@ -3,3 +3,14 @@
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
border: 2px solid #ffffff;
|
border: 2px solid #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.showLocale img {
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showUserSettings {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@ -4271,9 +4271,9 @@ normalize-url@^1.4.0:
|
|||||||
query-string "^4.1.0"
|
query-string "^4.1.0"
|
||||||
sort-keys "^1.0.0"
|
sort-keys "^1.0.0"
|
||||||
|
|
||||||
normalize.css@^7.0.0:
|
normalize.css@^8.0.0:
|
||||||
version "7.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-7.0.0.tgz#abfb1dd82470674e0322b53ceb1aaf412938e4bf"
|
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.0.tgz#14ac5e461612538a4ce9be90a7da23f86e718493"
|
||||||
|
|
||||||
npm-run-path@^2.0.0:
|
npm-run-path@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
@ -5035,9 +5035,9 @@ react-dom@^15.6.1:
|
|||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
prop-types "^15.5.10"
|
prop-types "^15.5.10"
|
||||||
|
|
||||||
react-mdl@^1.9.0:
|
react-mdl@^1.11.0:
|
||||||
version "1.10.3"
|
version "1.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-mdl/-/react-mdl-1.10.3.tgz#f783e26a5eea4154a32129ab2562c09d5eeacf0d"
|
resolved "https://registry.yarnpkg.com/react-mdl/-/react-mdl-1.11.0.tgz#7e07ee1009dd9b358b616dc400ff2ae1845a2e67"
|
||||||
dependencies:
|
dependencies:
|
||||||
clamp "^1.0.1"
|
clamp "^1.0.1"
|
||||||
classnames "^2.2.3"
|
classnames "^2.2.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user