diff --git a/README.md b/README.md index 7cf7e02b..d6617767 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,30 @@ [![CircleCI](https://circleci.com/gh/yldio/joyent-portal.svg?style=shield&circle-token=0bbeaaafc4868c707ca0ed0568f5193a04daddb4)](https://circleci.com/gh/yldio/joyent-portal) [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) -# Joyent Portal +# Protype Triton Portal -Before you begin, you will need to install ensure that `docker` and `docker-compose` are installed correctly, -this can be done by running `make`, make continues without any errors, then you are good to go. +This is a prototype project intended to explore some ideas that might contribute to new capabilities and a new user experience for managing applications on [Joyent's Triton](https://www.joyent.com/triton). + +**This is not intended for general use and is completely unsupported.** + +## Development + +If you would like to contribute to the project, the recommended way to setup is to +insure that you have docker installed, and optionally have a triton account and profile +setup using the triton tool. + +Currently requires [yarn](https://yarnpkg.com/en/docs/install) for installing dependencies, +as well as `docker` and `docker-compose` are installed correctly, this can be done by +running `make`, make continues without any errors, then you are good to go. + +``` +make && make install +``` + +Then to run each individual component locally (subject to change). ## Setup + ```sh make ``` @@ -22,20 +40,6 @@ docker-compose -f local-compose.yml up -d This will run the front-end at [http://127.0.0.1:8000](http://127.0.0.1:8000), the UI framework at [http://127.0.0.1:8001](http://127.0.0.1:8001), -## Development - -If you would like to contribute to the project, the recommended way to setup is to -insure that you have docker installed, and optionally have a triton account and profile -setup using the triton tool. - -Currently requires [yarn](https://yarnpkg.com/en/docs/install) for installing dependencies. - -``` -make && make install -``` - -Then to run each individual component locally (subject to change). - ## Project Management This project is using [Github Projects](https://www.youtube.com/watch?v=C6MGKHkNtxU) for organisation and development of the Joyent Dashboard. diff --git a/docker-compose.yml b/docker-compose.yml index 02ad5b0d..84f93985 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,24 +1,28 @@ ############################################################################# # CONSUL +# +# Consul is the service catalog that helps discovery between the components +# Change "-bootstrap" to "-bootstrap-expect 3", then scale to 3 or more to +# turn this into an HA Consul raft. ############################################################################# consul: - image: progrium/consul:latest + image: autopilotpattern/consul:latest + command: > + /usr/local/bin/containerpilot + /bin/consul agent -server + -bootstrap-expect 3 + -config-dir=/etc/consul + -ui-dir /ui + restart: always + mem_limit: 128m + env_file: .env + ports: + - 8500 + dns: + - 127.0.0.1 labels: - triton.cns.services=consul - com.docker.swarm.affinities=["container!=~*"] - restart: always - mem_limit: 128m - expose: - - 53 - - 8300 - - 8301 - - 8302 - - 8400 - - 8500 - env_file: .env - ports: - - 8500:8500 - command: -server -bootstrap -ui-dir /ui ############################################################################# # CloudAPI GraphQL ############################################################################# @@ -39,7 +43,7 @@ cloudapi: ############################################################################# frontend: image: quay.io/yldio/joyent-dashboard-frontend:latest - mem_limit: 256m + mem_limit: 512m labels: - triton.cns.services=frontend - com.docker.swarm.affinities=["container!=~*frontend*"] @@ -54,7 +58,7 @@ frontend: ############################################################################# ui: image: quay.io/yldio/joyent-dashboard-ui:latest - mem_limit: 128m + mem_limit: 512m labels: - triton.cns.services=ui - com.docker.swarm.affinities=["container!=~*ui*"] @@ -68,7 +72,7 @@ ui: nginx: image: quay.io/yldio/joyent-portal-nginx restart: always - mem_limit: 128m + mem_limit: 256m ports: - 80:80 - 443:443 diff --git a/frontend/src/components/people-list/index.js b/frontend/src/components/people-list/index.js index 0d791a02..765f6794 100644 --- a/frontend/src/components/people-list/index.js +++ b/frontend/src/components/people-list/index.js @@ -1,11 +1,11 @@ const React = require('react'); -const PropTypes = require('@root/prop-types'); const Row = require('@ui/components/row'); const Column = require('@ui/components/column'); const Button = require('@ui/components/button'); const PeopleTable = require('./table'); +const Invite = require('./invite'); const buttonStyle = { float: 'right' @@ -14,9 +14,8 @@ const buttonStyle = { const People = (props) => { const { - people = [], orgUI = {}, - handleToggle + handleToggle, } = props; return ( @@ -33,11 +32,11 @@ const People = (props) => { + {orgUI.invite_toggled ? : null} + - + @@ -46,8 +45,7 @@ const People = (props) => { People.propTypes = { handleToggle: React.PropTypes.func, - orgUI: React.PropTypes.obj, - people: React.PropTypes.arrayOf(PropTypes.person), + orgUI: React.PropTypes.object, }; module.exports = People; \ No newline at end of file diff --git a/frontend/src/components/people-list/invite.js b/frontend/src/components/people-list/invite.js new file mode 100644 index 00000000..2771f859 --- /dev/null +++ b/frontend/src/components/people-list/invite.js @@ -0,0 +1,107 @@ +const React = require('react'); + +// const PropTypes = require('@root/prop-types'); +const Row = require('@ui/components/row'); +const Column = require('@ui/components/column'); +const Button = require('@ui/components/button'); +// const SelectCustom = require('@ui/components/select-custom'); + +const Invite = (props) => { + + const { + // people = [], + handleToggle, + // platformMembers + } = props; + + // const InputStyle = { + // float: 'left', + // width: '75%' + // }; + + const AddButtonStyle = { + float: 'right', + width: '20%' + }; + + const styleInline = { + display: 'inline-block' + }; + + // const selectData = [ + // { + // value: 'one', + // label: 'One' + // }, + // { + // value: 'two', + // label: 'Two' + // }, + // { + // value: 'three', + // label: 'Three' + // }, + // { + // value: 'four', + // label: 'Four' + // }, + // { + // value: 'five', + // label: 'Five' + // }, + // { + // value: 'six', + // label: 'Six' + // } + // ]; + + return ( + + +

Search for a person by name or email or enter an email address + to invite someone new.

+ + + + {/*TODO: Fix why there are issues with webpack and nodemodules*/} + {/**/} + + + + + + + +
+
+ ); +}; + +Invite.propTypes = { + handleToggle: React.PropTypes.func, + // orgUI: React.PropTypes.obj, + // people: React.PropTypes.arrayOf(PropTypes.person) +}; + +module.exports = Invite; \ No newline at end of file diff --git a/frontend/src/components/people-list/table.js b/frontend/src/components/people-list/table.js deleted file mode 100644 index 0697d597..00000000 --- a/frontend/src/components/people-list/table.js +++ /dev/null @@ -1,47 +0,0 @@ -const React = require('react'); - -const PropTypes = require('@root/prop-types'); -const Table = require('@ui/components/table-data-table'); -const Checkbox = require('@ui/components/checkbox'); - -const PeopleTable = ({ - people = [] -}) => { - const columns = [{ - title: , - width: '5%' - }, { - title: 'Member', - width: '35%' - }, { - title: 'Status', - width: '25%' - }, { - title: 'Role', - width: '25%' - }, { - title: '', - width: '10%' // Empty title for delete - }]; - - const data = people.map( (person) => ({ - checkbox: , - name: person.name, - status: person.status, - role: person.role, - bin: '' - })); - - return ( - - ); -}; - -PeopleTable.propTypes = { - people: React.PropTypes.arrayOf(PropTypes.person) -}; - -module.exports = PeopleTable; \ No newline at end of file diff --git a/frontend/src/components/people-list/table/index.js b/frontend/src/components/people-list/table/index.js new file mode 100644 index 00000000..ddc7fb5c --- /dev/null +++ b/frontend/src/components/people-list/table/index.js @@ -0,0 +1,83 @@ +const React = require('react'); + +const Table = require('@ui/components/table-data-table'); +const Checkbox = require('@ui/components/checkbox'); + +const PersonStatus = require('./person-status'); +const PersonRole = require('./person-role'); + +const PeopleTable = (props) => { + + const { + handleRoleTooltip, + handleRoleUpdate, + handleStatusTooltip, + people = [], + orgUI = {} + } = props; + + const columns = [{ + title: , + width: '5%' + }, { + title: 'Member', + width: '35%' + }, { + title: 'Status', + width: '25%' + }, { + title: 'Role', + width: '25%' + }, { + title: '', + width: '10%' // Empty title for delete + }]; + + const data = people.map( (person, index) => { + const status = (person) => ( + + ); + + const role = (person) => ( + + ); + + return { + checkbox: , + name: person.name, + status: status(person), + role: role(person), + bin: '' + }; + }); + + return ( +
+ ); +}; + +PeopleTable.propTypes = { + handleRoleTooltip: React.PropTypes.func, + handleRoleUpdate: React.PropTypes.func, + handleStatusTooltip: React.PropTypes.func, + orgUI: React.PropTypes.object, + people: React.PropTypes.array, +}; + +module.exports = PeopleTable; \ No newline at end of file diff --git a/frontend/src/components/people-list/table/person-role.js b/frontend/src/components/people-list/table/person-role.js new file mode 100644 index 00000000..9fb2c543 --- /dev/null +++ b/frontend/src/components/people-list/table/person-role.js @@ -0,0 +1,97 @@ +const React = require('react'); +const Styled = require('styled-components'); + +const fns = require('@ui/shared/functions'); +const composers = require('@ui/shared/composers'); + +const Tooltip = require('./tooltip'); + +const { + pseudoEl +} = composers; + +const { + default: styled +} = Styled; + +const { + remcalc +} = fns; + +const borderSide = props => props.toggled + ? 'bottom' + : 'top'; + +const StyledWrapper = styled.div` + position: relative; + + &:after { + border-left: ${remcalc(5)} solid transparent; + border-right: ${remcalc(5)} solid transparent; + border-${borderSide}: ${remcalc(5)} solid black; + + ${pseudoEl({ + top: '50%', + right: remcalc(10) + })} + } +`; + +const PlainButton = styled.button` + background: transparent; + font-size: inherit; + border: none; + zIndex: 0; + font-family: inherit; + color: inherit; +`; + +const PersonRole = (props) => { + + const { + toggledID, + membersRolesOptions, + person, + personIndex, + handleRoleTooltip, + handleRoleUpdate + } = props; + + const toggled = toggledID; + const handleClick = () => handleRoleTooltip(person.uuid); + const handleOptionSelect = (updatedMember) => handleRoleUpdate(updatedMember); + const _person = { + ...person, + personIndex + }; + + return ( + + + {person.role} + + + { toggledID === person.uuid + ? + : null } + + ); +}; + +PersonRole.propTypes = { + handleRoleTooltip: React.PropTypes.func, + handleRoleUpdate: React.PropTypes.func, + membersRolesOptions: React.PropTypes.array, + person: React.PropTypes.object, + personIndex: React.PropTypes.number, + toggledID: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.bool, + ]) +}; + +module.exports = PersonRole; \ No newline at end of file diff --git a/frontend/src/components/people-list/table/person-status.js b/frontend/src/components/people-list/table/person-status.js new file mode 100644 index 00000000..4aa1929a --- /dev/null +++ b/frontend/src/components/people-list/table/person-status.js @@ -0,0 +1,84 @@ +const React = require('react'); +const Styled = require('styled-components'); + +const fns = require('@ui/shared/functions'); +const composers = require('@ui/shared/composers'); + +const Tooltip = require('./tooltip'); + +const { + pseudoEl +} = composers; + +const { + default: styled +} = Styled; + +const { + remcalc +} = fns; + +const borderSide = props => props.toggled + ? 'bottom' + : 'top'; + +const StyledWrapper = styled.div` + position: relative; + + &:after { + border-left: ${remcalc(5)} solid transparent; + border-right: ${remcalc(5)} solid transparent; + border-${borderSide}: ${remcalc(5)} solid black; + + ${pseudoEl({ + top: '50%', + right: remcalc(10) + })} + } +`; + +const PlainButton = styled.button` + background: transparent; + font-size: inherit; + border: none; + zIndex: 0; + font-family: inherit; + color: inherit; +`; + +const PersonStatus = (props) => { + + const { + handleStatusTooltip, + toggledID, + membersStatusOptions, + person, + } = props; + + const toggled = toggledID; + const handleClick = () => handleStatusTooltip(person.uuid); + + return ( + + + {person.status} + + + { toggledID === person.uuid + ? + : null } + + ); +}; + +PersonStatus.propTypes = { + handleStatusTooltip: React.PropTypes.func, + membersStatusOptions: React.PropTypes.array, + person: React.PropTypes.object, + toggledID: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.bool, + ]) +}; + +module.exports = PersonStatus; \ No newline at end of file diff --git a/frontend/src/components/people-list/table/tooltip.js b/frontend/src/components/people-list/table/tooltip.js new file mode 100644 index 00000000..69a5681b --- /dev/null +++ b/frontend/src/components/people-list/table/tooltip.js @@ -0,0 +1,56 @@ +const React = require('react'); + +const Tooltip = require('@ui/components/tooltip'); + +const tooltipStyle = { + position: 'absolute', + top: '30px', + zIndex: 1 +}; + +const arrowPosition = { + bottom: '100%', + right: '10%' +}; + +module.exports = ({ + handleSelect, + person = {}, + options = [], +}) => { + + const _options = options.map( (option, i) => { + + const _onClick = () => handleSelect({ + ...person, + role: option + }); + + return ( +
  • + {option} +
  • + ); + }); + + return ( + + {_options} + + ); +}; + +module.exports.propTypes = { + handleSelect: React.PropTypes.func, + options: React.PropTypes.array, + person: React.PropTypes.object, +}; \ No newline at end of file diff --git a/frontend/src/containers/org/people.js b/frontend/src/containers/org/people.js index a9ca21b7..3e56bac1 100644 --- a/frontend/src/containers/org/people.js +++ b/frontend/src/containers/org/people.js @@ -11,11 +11,15 @@ const { const { peopleByOrgIdSelector, - orgUISelector + orgUISelector, + membersSelector } = selectors; const { - handleInviteToggle + handleInviteToggle, + handlePeopleRoleTooltip, + handlePeopleStatusTooltip, + handleRoleUpdate } = actions; const People = (props) => { @@ -31,11 +35,16 @@ const mapStateToProps = (state, { params = {} }) => ({ people: peopleByOrgIdSelector(params.org)(state), - orgUI: orgUISelector(state) + orgUI: orgUISelector(state), + platformMembers: membersSelector(state) }); const mapDispatchToProps = (dispatch) => ({ - handleToggle: () => dispatch(handleInviteToggle()) + handleToggle: () => dispatch(handleInviteToggle()), + handleStatusTooltip: (id) => dispatch(handlePeopleStatusTooltip(id)), + handleRoleTooltip: (id) => dispatch(handlePeopleRoleTooltip(id)), + handleRoleUpdate: (updatedMember) => + dispatch(handleRoleUpdate(updatedMember)), }); module.exports = connect( diff --git a/frontend/src/mock-state.json b/frontend/src/mock-state.json index 56b6e574..12d5c2d5 100644 --- a/frontend/src/mock-state.json +++ b/frontend/src/mock-state.json @@ -479,11 +479,23 @@ }, "orgs": { "ui": { - "invite_toggled": false, + "invite_toggled": true, + "member_status_tooltip": false, + "member_role_tooltip": false, "sections": [ "projects", "people", "settings" + ], + "members_status": [ + "Active", + "Inactive", + "Invitation Sent" + ], + "members_roles": [ + "Owner", + "Unnassigned", + "Read Only" ] }, "data": [{ diff --git a/frontend/src/state/actions.js b/frontend/src/state/actions.js index 1cf6563e..0ee97f57 100644 --- a/frontend/src/state/actions.js +++ b/frontend/src/state/actions.js @@ -17,5 +17,10 @@ module.exports = { toggleInstanceCollapsed: createAction(`${APP}/TOGGLE_INSTANCE_COLLAPSED`), toggleMonitorView: createAction(`${APP}/TOGGLE_MONITOR_VIEW`), switchMonitorViewPage: createAction(`${APP}/SWITCH_MONITOR_VIEW_PAGE`), - handleInviteToggle: createAction(`${APP}/HANDLE_INVITE_MEMBER_TOGGLE`) + handleInviteToggle: createAction(`${APP}/HANDLE_INVITE_MEMBER_TOGGLE`), + handlePeopleStatusTooltip: + createAction(`${APP}/HANDLE_PERSON_STATUS_TOOLTIP`), + handlePeopleRoleTooltip: + createAction(`${APP}/HANDLE_PERSON_ROLE_TOOLTIP`), + handleRoleUpdate: createAction(`${APP}/HANDLE_PERSON_ROLE_UPDATE`), }; diff --git a/frontend/src/state/reducers/orgs.js b/frontend/src/state/reducers/orgs.js index 7fec06fb..e7ce272b 100644 --- a/frontend/src/state/reducers/orgs.js +++ b/frontend/src/state/reducers/orgs.js @@ -1,9 +1,76 @@ const ReduxActions = require('redux-actions'); +const actions = require('@state/actions'); + const { handleActions } = ReduxActions; +const { + handleInviteToggle, + handlePeopleRoleTooltip, + handlePeopleStatusTooltip, + handleRoleUpdate +} = actions; + module.exports = handleActions({ - 'x': (state) => state // somehow handleActions needs at least one reducer + [handleInviteToggle.toString()]: (state, action) => { + return { + ...state, + ui: { + ...state.ui, + invite_toggled: !state.ui.invite_toggled + } + }; + }, + [handlePeopleStatusTooltip.toString()]: (state, action) => { + return { + ...state, + ui: { + ...state.ui, + member_status_tooltip: + action.payload === state.ui.member_status_tooltip + ? '' + : action.payload + } + }; + }, + [handlePeopleRoleTooltip.toString()]: (state, action) => { + return { + ...state, + ui: { + ...state.ui, + member_role_tooltip: + action.payload === state.ui.member_role_tooltip + ? '' + : action.payload + } + }; + }, + [handleRoleUpdate.toString()]: (state, action) => { + // TODO: + // Change "1" to org index. At the moment only updates + // "biz-tech" + return { + ...state, + ui: { + ...state.ui, + member_role_tooltip: false + }, + data: [ + ...state.data.slice(0, 1), + { + ...state.data[1], + members: [ + ...state.data[1].members.slice(0, action.payload.personIndex), + { + ...action.payload + }, + ...state.data[1].members.slice(action.payload.personIndex + 1) + ] + }, + ...state.data.slice(1+1), + ] + }; + } }, {}); diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index ff6ee9d0..9e6700c9 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -153,5 +153,6 @@ module.exports = { metricTypesSelector: metricTypes, instancesByProjectIdSelector: instancesByProjectId, metricTypeByUuidSelector: metricTypeByUuid, - peopleByOrgIdSelector: peopleByOrgId + peopleByOrgIdSelector: peopleByOrgId, + membersSelector: members, }; diff --git a/frontend/static/.gitignore b/frontend/static/.gitignore index a780b368..564e4fda 100644 --- a/frontend/static/.gitignore +++ b/frontend/static/.gitignore @@ -1,3 +1,4 @@ * !.gitignore !locales +!images diff --git a/frontend/static/images/avatar.png b/frontend/static/images/avatar.png new file mode 100644 index 00000000..68b6ad91 Binary files /dev/null and b/frontend/static/images/avatar.png differ diff --git a/frontend/static/images/make-us-proud.svg b/frontend/static/images/make-us-proud.svg new file mode 100644 index 00000000..f2d5d04e --- /dev/null +++ b/frontend/static/images/make-us-proud.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/webpack/base.js b/frontend/webpack/base.js index 89c61179..003fa7b5 100644 --- a/frontend/webpack/base.js +++ b/frontend/webpack/base.js @@ -86,12 +86,15 @@ module.exports = { }, { test: /\.css$/, - exclude: /node_modules/, - loaders: [ 'style-loader', 'css-loader' ], - include: [ - FRONTEND, - UI - ] + loader: 'style-loader!css-loader' + // XXX: Commenting out breaks node_modules that use css + // i.e react-select. + + // exclude: /node_modules/, + // include: [ + // FRONTEND, + // UI + // ] }] } }; diff --git a/ui/src/components/select-custom/index.js b/ui/src/components/select-custom/index.js index d4a7a81b..f2bd9bca 100644 --- a/ui/src/components/select-custom/index.js +++ b/ui/src/components/select-custom/index.js @@ -38,11 +38,12 @@ const SelectCustom = ({ onChange, options, required = false, + style, value = '' }) => { return ( -
    +
    {label} @@ -76,6 +77,7 @@ SelectCustom.propTypes = { onChange: React.PropTypes.func, options: React.PropTypes.array, required: React.PropTypes.bool, + style: React.PropTypes.object, value: React.PropTypes.string }; diff --git a/ui/src/components/tooltip/index.js b/ui/src/components/tooltip/index.js index cb4ab090..56fa0d90 100644 --- a/ui/src/components/tooltip/index.js +++ b/ui/src/components/tooltip/index.js @@ -3,6 +3,7 @@ const React = require('react'); const composers = require('../../shared/composers'); const fns = require('../../shared/functions'); +const constants = require('../../shared/constants'); const Styled = require('styled-components'); const { @@ -18,6 +19,10 @@ const { default: styled } = Styled; +const { + colors +} = constants; + const ItemPadder = 9; const WrapperPadder = 24; const ulPadder = `${WrapperPadder - ItemPadder} 0`; @@ -33,6 +38,8 @@ const StyledList = styled.ul` margin: 0; padding: ${remcalc(ulPadder)}; min-width: ${remcalc(200)}; + + ${props => props.style} ${baseBox()} @@ -41,7 +48,7 @@ const StyledList = styled.ul` padding: ${remcalc(ItemPadder)} ${remcalc(WrapperPadder)}; &:hover { - background: red; + background: ${colors.borderSecondaryDarkest}; } } @@ -79,6 +86,7 @@ module.exports = ({ {children}