Rock and roll

This commit is contained in:
JUDIT GRESKOVITS 2017-05-11 18:16:52 +01:00 committed by Sérgio Ramos
parent 131923ca59
commit 7e2bfce707
196 changed files with 12972 additions and 4818 deletions

View File

@ -0,0 +1,50 @@
[![Docker Repository on Quay](https://quay.io/repository/yldio/joyent-dashboard-frontend/status?token=ab124f42-c6c5-4023-9841-17ef74c8b7f1 "Docker Repository on Quay")](https://quay.io/repository/yldio/joyent-dashboard-frontend)
# Joyent Dashboard Frontend
## start
```
npm run start
```
## test
```
npm run test
```
## structure
```
.
├── src
│   ├── containers
│   ├── index.js
│   ├── root.js
│   └── state
│   ├── actions.js
│   ├── reducers
│   ├── store.js
│   └── thunks
├── static
├── locales
├── scripts
├── test
├── webpack
├── .babelrc
└── .eslintrc
```
- **src/index.js**: Renders `src/root.js` and bootstraps hot module reloading.
- **src/root.js**: The main component that wraps `react-redux`, `react-router` and `react-hot-loader`.
- **src/state/store.js**: Exports a function that creates a `redux` store instance with all the middlewares and reducers configured.
- **src/state/actions.js**: Not only exports all the actions available (declared in the file), but also goes through all the thunks and exports them.
- **src/state/thunks**: Directory to place thunks so that actions or reducers don't get too confusing.
- **src/state/reducers**: Each file here represents a reducer scope. So, `state.app` will be controlled in `reducers/app.js`.
- **locales**: Translation definitions for each locale supported.
- **scripts**: Utility scripts (e.g. building localizations).
- **test**: Self explanatory.
- **webpack**: Webpack configuration for multiple enviroments. Development configuration includes a dev-server and hot module replacement support.
- **.babelrc**: This babel configuration outputs ES2015 code, so it will produce code only for modern browsers.
Also, async/await is supported.
- **.eslintrc**:ESLint configuration. It's basically [semistandard](https://github.com/Flet/semistandard) with `space-before-function-paren` probited;

View File

@ -0,0 +1,134 @@
{
"name": "joyent-portal-frontend",
"version": "1.1.0",
"private": true,
"license": "private",
"main": "server/index.js",
"directories": {
"test": "test",
"lib": "src"
},
"scripts": {
"start": "webpack-dev-server --define process.env.BASELINE_GRID=false --open --config webpack/index.js ",
"production": "node server",
"lint": "make lint",
"lint-fix": "make lint-fix",
"test": "make test",
"open": "nyc report --reporter=html & open coverage/index.html",
"coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
"build-locales": " CONFIG=$(pwd)/webpack/index.js babel-node scripts/build-locales",
"clean-static": "git check-ignore static/** | xargs rm"
},
"dependencies": {
"@tomgco/joyent-portal-ui": "alpha",
"constant-case": "^2.0.0",
"force-array": "^3.1.0",
"hapi": "^16.1.0",
"inert": "^4.1.0",
"inherits": "^2.0.3",
"lodash.find": "^4.6.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"lodash.template": "^4.4.0",
"moment": "^2.17.1",
"param-case": "^2.1.0",
"portal-api": "^1.0.0",
"querystring": "^0.2.0",
"react": "^15.4.2",
"react-a11y": "^0.3.3",
"react-dom": "^15.4.2",
"react-intl": "^2.2.3",
"react-intl-redux": "^0.4.1",
"react-redux": "^5.0.3",
"react-router": "4.0.0-beta.6",
"react-router-dom": "4.0.0-beta.6",
"react-select": "^1.0.0-rc.3",
"reduce-reducers": "^0.1.2",
"redux": "^3.6.0",
"redux-actions": "^1.2.2",
"redux-batched-actions": "^0.1.5",
"redux-form": "^6.5.0",
"redux-logger": "^2.8.1",
"redux-promise-middleware": "^4.2.0",
"redux-thunk": "^2.2.0",
"reselect": "^2.5.4",
"simple-statistics": "^2.5.0",
"styled-components": "^1.4.4",
"understood": "^1.0.1"
},
"devDependencies": {
"ava": "^0.18.2",
"babel-cli": "^6.23.0",
"babel-core": "^6.23.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.3.2",
"babel-plugin-inline-react-svg": "^0.2.0",
"babel-plugin-styled-components": "^1.0.0",
"babel-plugin-transform-class-properties": "^6.23.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.23.0",
"babel-plugin-transform-es2015-parameters": "^6.23.0",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-plugin-transform-react-jsx-self": "^6.22.0",
"babel-plugin-transform-react-jsx-source": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-plugin-webpack-alias": "^2.1.2",
"babel-plugin-webpack-loaders": "^0.9.0",
"babel-preset-env": "^1.1.10",
"babel-preset-react": "^6.23.0",
"babel-register": "^6.23.0",
"case-sensitive-paths-webpack-plugin": "^1.1.4",
"css-loader": "^0.26.2",
"enzyme": "^2.7.1",
"eslint": "^3.16.1",
"eslint-config-semistandard": "^7.0.0",
"eslint-config-standard": "^7.0.0",
"eslint-plugin-babel": "^4.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^6.10.0",
"eslint-plugin-standard": "^2.0.1",
"file-loader": "^0.10.1",
"jsdom": "^9.11.0",
"json-loader": "^0.5.4",
"ncp": "^2.0.0",
"node-hook": "^0.4.0",
"nyc": "^10.1.2",
"pre-commit": "^1.2.2",
"react-addons-perf": "^15.4.2",
"react-addons-test-utils": "^15.4.2",
"react-dev-utils": "^0.5.1",
"react-perf": "^1.0.1",
"redux-ava": "^2.2.0",
"redux-perf-middleware": "^1.2.2",
"require-hacker": "^2.1.4",
"simple-mock": "^0.7.3",
"style-loader": "^0.13.2",
"stylelint": "^7.9.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-processor-styled-components": "^0.0.4",
"tap-xunit": "^1.7.0",
"thenify": "^3.2.1",
"url-loader": "^0.5.8",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.4.1",
"webpack-manifest-plugin": "^1.1.0",
"webpack-shell-plugin": "^0.5.0"
},
"ava": {
"failFast": true,
"cache": false,
"require": [
"./test/_hook.js"
],
"babel": "inherit"
},
"pre-commit": [
"lint",
"test",
"coverage"
]
}

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
import Container from '@ui/components/container';
import { breakpoints } from '@ui/shared/constants';
const LayoutContainer = styled(Container)`
padding: 2rem;
${breakpoints.large`
padding: 0;
`}
`;
export default LayoutContainer;

View File

@ -0,0 +1 @@
export { default as LayoutContainer } from './container';

View File

@ -0,0 +1,79 @@
import React from 'react';
import styled from 'styled-components';
import flatten from 'lodash.flatten';
import Container from '@ui/components/container';
import Row from '@ui/components/row';
import Column from '@ui/components/column';
import { H2 } from '@ui/components/base-elements';
import NavLink from '@ui/components/nav-link';
import PropTypes from '@root/prop-types';
import { remcalc, unitcalc } from '@ui/shared/functions';
import { colors } from '@ui/shared/constants';
// Main Contonent Wrapper Styles
const StyledDiv = styled.div`
border-bottom: solid ${remcalc(1)} ${colors.base.grey};
padding: ${unitcalc(4.5)} 0 ${unitcalc(4.5)} 0;
margin-bottom: ${remcalc(18)};
`;
const BreadcrumbA = styled(NavLink)`
text-decoration: none;
color: ${colors.base.primary};
`;
const BreadcrumbSpan = styled.span`
color: ${colors.base.text};
`;
function getNameLink(name) {
return flatten(name.map((part, i) => {
if (!part.name) {
return null;
}
const link = (
<BreadcrumbA key={part.pathname} to={part.pathname}>
{part.name}
</BreadcrumbA>
);
const key = `${part.pathname}${i}`;
const slash = (
<BreadcrumbSpan key={key}> / </BreadcrumbSpan>
);
return (i === 0) ? link : [
slash,
link
];
}));
}
const StyledH2 = styled(H2)`
color: ${colors.base.primary};
margin: 0;
`;
const Breadcrumb = ({
name = []
}) => (
<Container>
<Row>
<Column xs={12}>
<StyledDiv>
<StyledH2>
{getNameLink(name)}
</StyledH2>
</StyledDiv>
</Column>
</Row>
</Container>
);
Breadcrumb.propTypes = {
name: React.PropTypes.arrayOf(PropTypes.link)
};
export default Breadcrumb;

View File

@ -0,0 +1,2 @@
export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';

View File

@ -0,0 +1,55 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import styled from 'styled-components';
import Li from '@ui/components/horizontal-list/li';
import NavLink from '@ui/components/nav-link';
import { breakpoints } from '@ui/shared/constants';
import { remcalc } from '@ui/shared/functions';
import PropTypes from '@root/prop-types';
import Ul from '@ui/components/horizontal-list/ul';
import { LayoutContainer } from '@components/layout';
const StyledHorizontalList = styled(Ul)`
padding: 0;
`;
const StyledHorizontalListItem = styled(Li)`
${breakpoints.smallOnly`
display: block;
`}
& + li {
margin-left: ${remcalc(21)};
}
`;
const Menu = ({
links
}) => {
const navLinks = links.map((link) => {
return (
<StyledHorizontalListItem key={link.name}>
<NavLink activeClassName='active' to={link.pathname}>
<FormattedMessage id={link.name} />
</NavLink>
</StyledHorizontalListItem>
);
});
// TODO this could be any kind of nav, not just 'project-'...
return (
<LayoutContainer>
<StyledHorizontalList name='project-nav'>
{navLinks}
</StyledHorizontalList>
</LayoutContainer>
);
};
Menu.propTypes = {
links: React.PropTypes.arrayOf(PropTypes.link)
};
export default Menu;

View File

@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import {
orgByIdSelector,
projectByIdSelector,
serviceByIdSelector
} from '@root/state/selectors';
import { Breadcrumb as BreadcrumbComponent } from '@components/navigation';
const Breadcrumb = ({
location,
match,
org,
project,
service
}) => {
const path = location.pathname.split('/');
const links = [{
name: org.name,
pathname: path.slice(0, 2).join('/')
}];
if(project) {
links.push({
name: project.name,
pathname: path.slice(0, 4).join('/')
});
}
if(service) {
links.push({
name: service.name,
pathname: path.slice(0, 6).join('/')
});
}
// TODO add people etc
return (
<BreadcrumbComponent name={links} />
);
};
Breadcrumb.propTypes = {
location: React.PropTypes.object.isRequired,
match: React.PropTypes.object.isRequired,
org: React.PropTypes.object.isRequired,
project: React.PropTypes.object,
service: React.PropTypes.object
};
const mapStateToProps = (state, {
location,
match = {
params: {}
}
}) => ({
location,
match,
org: orgByIdSelector(match.params.org)(state),
project: projectByIdSelector(match.params.project)(state),
service: serviceByIdSelector(match.params.service)(state)
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Breadcrumb);

View File

@ -0,0 +1,3 @@
export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';
export { default as Org } from './org';

View File

@ -0,0 +1,51 @@
import React from 'react';
import { connect } from 'react-redux';
import {
orgSectionsByIdSelector,
projectSectionsSelector,
serviceSectionsSelector
} from '@root/state/selectors';
import { Menu as MenuComponent } from '@components/navigation';
const Menu = (props) => {
const {
match,
sections
} = props;
const links = sections.map((section) => ({
name: section,
pathname: `${match.url}/${section}`
}));
return (
<MenuComponent links={links} />
);
};
Menu.propTypes = {
match: React.PropTypes.object.isRequired,
sections: React.PropTypes.array.isRequired
};
const mapStateToProps = (state, {
match = {
params: {}
}
}) => ({
location,
match,
sections: match.params.service ?
serviceSectionsSelector(state) :
match.params.project ?
projectSectionsSelector(state) :
orgSectionsByIdSelector(match.params.org)(state)
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Menu);

View File

@ -0,0 +1,8 @@
export { default as ServiceActivityFeed } from './activity-feed';
export { default as ServiceFirewall } from './firewall';
export { default as ServiceInstances } from './instances';
export { default as ServiceMetrics } from './metrics';
export { default as ServiceNetworks } from './networks';
export { default as ServiceManifest } from './service-manifest';
export { default as ServiceSummary } from './summary';
export { default as ServiceMetadata } from './tags-metadata';

View File

@ -0,0 +1,46 @@
import { connect } from 'react-redux';
import { addMetric, metricDurationChange } from '@state/actions';
import Metrics from '@containers/metrics';
import {
metricsByServiceIdSelector,
metricTypesSelector,
serviceByIdSelector
} from '@state/selectors';
const mapStateToProps = (state, {
match = {
params: {}
}
}) => ({
datasets: metricsByServiceIdSelector(match.params.service)(state),
metricTypes: metricTypesSelector(state),
service: serviceByIdSelector(match.params.service)(state)
});
const mapDispatchToProps = (dispatch) => ({
addMetric: (service) => (metric) =>
dispatch(addMetric({
metric: metric,
service: service.uuid
})),
metricDurationChange: (service) => (duration, dataset) =>
dispatch(metricDurationChange({
duration,
dataset
}))
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
addMetric: dispatchProps.addMetric(stateProps.service),
metricDurationChange: dispatchProps.metricDurationChange(stateProps.service)
});
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(Metrics);

View File

@ -0,0 +1,3 @@
export { default as ServicesTopology } from './topology';
export { default as ServicesList } from './list';
export { default as ServicesView } from './view';

View File

@ -0,0 +1,138 @@
import React from 'react';
import styled from 'styled-components';
import { connect } from 'react-redux';
import PropTypes from '@root/prop-types';
import { LayoutContainer } from '@components/layout';
import ServiceItem from '@components/service/item';
import UnmanagedInstances from '@components/services/unmanaged-instances';
import { toggleTooltip } from '@state/actions';
import ServicesTooltip from '@components/services/tooltip';
import { subscribeMetric } from '@state/thunks';
import {
orgByIdSelector,
projectByIdSelector,
servicesByProjectIdSelector,
serviceUiTooltipSelector
} from '@state/selectors';
const StyledContainer = styled.div`
position: relative;
`;
// TMP - single source of truth
const duration = '5 minutes';
const interval = '15 seconds';
class Services extends React.Component {
// we DON'T want to unsubscribe once we started going
componentWillMount() {
this.props.subscribeMetric(interval);
}
ref(name) {
this._refs = this._refs || {};
return (el) => {
this._refs[name] = el;
};
}
render() {
const {
org = {},
project = {},
services = [],
toggleTooltip = () => ({}),
uiTooltip = {}
} = this.props;
const onQuickActions = (evt, service) => {
const list = this._refs.container;
const listRect = list.getBoundingClientRect();
const button = evt.currentTarget;
const buttonRect = button.getBoundingClientRect();
const position = {
left: buttonRect.left - listRect.left
+ (buttonRect.right - buttonRect.left)/2,
top: buttonRect.bottom - listRect.top
};
toggleTooltip({
service: service,
position: position,
data: {
serviceId: service.id,
orgId: org.id,
projectId: project.id
}
});
};
const handleTooltipBlur = (evt) => onQuickActions(evt, uiTooltip.service);
const serviceList = services.map((service) => (
<ServiceItem
key={service.uuid}
onQuickActions={onQuickActions}
org={org.id}
project={project.id}
service={service}
uiTooltip={uiTooltip}
/>
));
// TODO replace `false` with a check for existence unmanaged instances
// eslint-disable-next-line no-constant-condition
const unmanagedInstances = false ? (
<UnmanagedInstances instances={0} />
) : null;
return (
<LayoutContainer>
{unmanagedInstances}
<StyledContainer>
<div ref={this.ref('container')}>
{serviceList}
<ServicesTooltip {...uiTooltip} onBlur={handleTooltipBlur} />
</div>
</StyledContainer>
</LayoutContainer>
);
}
}
Services.propTypes = {
org: PropTypes.org,
project: PropTypes.project,
services: React.PropTypes.arrayOf(PropTypes.service),
toggleTooltip: React.PropTypes.func,
uiTooltip: React.PropTypes.object,
subscribeMetric: React.PropTypes.func
};
const mapStateToProps = (state, {
match = {
params: {}
},
push
}) => ({
org: orgByIdSelector(match.params.org)(state),
project: projectByIdSelector(match.params.project)(state),
services: servicesByProjectIdSelector(match.params.project, {
duration,
interval
})(state),
uiTooltip: serviceUiTooltipSelector(state)
});
const mapDispatchToProps = (dispatch) => ({
toggleTooltip: (data) => dispatch(toggleTooltip(data)),
subscribeMetric: (payload) => dispatch(subscribeMetric(payload))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Services);

View File

@ -0,0 +1,120 @@
import { IntlProvider } from 'react-intl-redux';
import { Provider } from 'react-redux';
import qs from 'querystring';
import a11y from 'react-a11y';
import ReactDOM from 'react-dom';
import React from 'react';
import Perf from 'react-addons-perf';
import MockStateTesting from '@mock-states/testing';
import MockState from '@mock-states';
import LeakDatasets from './dataset-leak.json';
import NormalDatasets from './dataset-normal.json';
import Store from '@state/store';
import { isProduction } from '@utils';
import Router from '@root/routing';
if ( !isProduction() ) {
a11y(React, {
ReactDOM
});
window.Perf = Perf;
}
const states = {
all: MockState,
testing: MockStateTesting
};
const query = qs.parse(window.location.search.replace(/^\?/, ''));
const mockState = states[query.mock || 'all'];
// node_memory_rss_bytes
// node_memory_heap_total_bytes
// node_memory_heap_used_bytes
// process_heap_bytes
// process_resident_memory_bytes
// process_virtual_memory_bytes
// process_cpu_seconds_total
// process_cpu_system_seconds_total
// process_cpu_user_seconds_total
// node_lag_duration_milliseconds
// http_request_duration_milliseconds
// node_memory_rss_bytes
// node_memory_heap_total_bytes
// node_memory_heap_used_bytes
// process_heap_bytes
// process_resident_memory_bytes
// process_virtual_memory_bytes
// process_cpu_seconds_total
// process_cpu_system_seconds_total
// process_cpu_user_seconds_total
// node_lag_duration_milliseconds
// http_request_duration_milliseconds
// TMP - ensure datasets are at least 2 hrs long - START
import getTwoHourDatasets from './utils/two-hour-metric-datasets';
const leakTwoHourLongDatasets = getTwoHourDatasets(LeakDatasets);
const normalTwoHourLongDatasets = getTwoHourDatasets(NormalDatasets);
// TMP - ensure datasets are at least 2 hrs long - END
// TMP - plug fake metric data - START
const isCrazy = (uuid) => uuid === 'crazy-cpu' ||
uuid === 'crazy-disk' || uuid === 'crazy-memory';
const isCPU = (uuid) => uuid === 'crazy-cpu'
|| uuid === '3e6ee79a-7453-4fc6-b9da-7ae1e41138ec';
const isDisk = (uuid) => uuid === 'crazy-disk'
|| uuid === '4e6ee79a-7453-4fc6-b9da-7ae1e41138ed';
const isMemory = (uuid) => uuid === 'crazy-memory'
|| uuid === '6e6ee79a-7453-4fc6-b9da-7ae1e41138ed';
const getDataset = (twoHourLongDatasets, uuid) => {
if(isCPU(uuid)) {
return twoHourLongDatasets.process_cpu_seconds_total;
}
if(isDisk(uuid)) {
return twoHourLongDatasets.process_heap_bytes.map((sample) =>
[
sample[0],
sample[1]/1024/1024
]
);
}
if(isMemory(uuid)) {
return twoHourLongDatasets.node_memory_heap_used_bytes.map((sample) =>
[
sample[0],
sample[1]/1024/1024
]
);
}
};
const datasets = MockState.metrics.data.datasets.map((dataset, index) => {
const data = isCrazy(dataset.uuid) && dataset.uuid !== 'crazy-cpu' ?
getDataset(leakTwoHourLongDatasets, dataset.uuid) :
getDataset(normalTwoHourLongDatasets, dataset.uuid);
return {
...dataset,
data: data
};
});
mockState.metrics.data.datasets = datasets;
// TMP - plug fake metric data - END
ReactDOM.render(
<Provider store={Store(mockState)}>
<IntlProvider>
{Router}
</IntlProvider>
</Provider>,
document.getElementById('root')
);

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