diff --git a/app/mixins/github-user-info.js b/app/mixins/github-user-info.js index 0e2791eb6..0bf1a11b4 100644 --- a/app/mixins/github-user-info.js +++ b/app/mixins/github-user-info.js @@ -6,22 +6,45 @@ export default Ember.Mixin.create({ login: null, size: 40, - name: 'Loading...', + name: null, + description: 'Loading...', _avatarUrl: null, loginOrTypeChanged: function() { var self = this; + var session = this.get('session'); - var cache = self.get('session').get('avatarCache')||{}; - + var cache = session.get('avatarCache')||{}; var login = this.get('login'); - if ( !login ) + var type = this.get('type'); + var key = type + ':' + login; + + if ( !type || !login ) { return; } - var type = this.get('type'); - var key = type + ':' + login; + // Teams can't be looked up without auth... + if ( type === 'team' ) + { + var entry = (session.get('teams')||[]).filterProperty('name', login)[0]; + this.set('_avatarUrl', null); + if ( entry ) + { + this.set('name', entry.name); + this.set('description', entry.org + ' team'); + } + else + { + this.set('name', '('+ login + ')'); + this.set('description', '(Unknown team id)'); + } + + return; + } + + this.set('name', login); + if ( cache[key] ) { @@ -30,21 +53,21 @@ export default Ember.Mixin.create({ else { var url = C.GITHUB_API_URL + type + 's/' + login; - Ember.$.ajax({url: url, dataType: 'json'}).then(function(body) { + Ember.$.ajax({url: url, dataType: 'json'}).then((body) => { cache[key] = body; // Sub-keys don't get automatically persisted to the session... - self.get('session').set('avatarCache', cache); + session.set('avatarCache', cache); gotInfo(body); - }, function() { - self.sendAction('notFound', login); + }, () => { + this.sendAction('notFound', login); }); } function gotInfo(body) { - self.set('name', body.name); + self.set('description', body.name); self.set('_avatarUrl', body.avatar_url); } }.observes('login','type').on('init'), diff --git a/app/pods/apikeys/template.hbs b/app/pods/apikeys/template.hbs index d37037407..e00e23a03 100644 --- a/app/pods/apikeys/template.hbs +++ b/app/pods/apikeys/template.hbs @@ -16,7 +16,7 @@
-
+
diff --git a/app/pods/components/github-block/component.js b/app/pods/components/github-block/component.js index 1447e9880..d58e11b54 100644 --- a/app/pods/components/github-block/component.js +++ b/app/pods/components/github-block/component.js @@ -4,4 +4,7 @@ import GithubUserInfoMixin from 'ui/mixins/github-user-info'; export default Ember.Component.extend(GithubUserInfoMixin,{ classNames: ['gh-block'], avatar: true, + link: true, + + isTeam: Ember.computed.equal('type','team'), }); diff --git a/app/pods/components/github-block/template.hbs b/app/pods/components/github-block/template.hbs index e3b2dd404..eeb49b5d8 100644 --- a/app/pods/components/github-block/template.hbs +++ b/app/pods/components/github-block/template.hbs @@ -2,21 +2,39 @@ {{#if avatar}}
{{#if avatarUrl}} - + {{#if link}} + + + + {{else}} - + {{/if}} {{else}} -
+ {{#if isTeam}} +
+ +
+ {{else}} +
+ {{/if}} {{/if}}
{{/if}}
- {{login}} + {{#if isTeam}} + {{name}} + {{else}} + {{#if link}} + {{name}} + {{else}} + {{name}} + {{/if}} + {{/if}}
- {{#if name}} - {{name}} + {{#if description}} + {{description}} {{else}} No Name {{/if}} diff --git a/app/pods/components/host-widget/template.hbs b/app/pods/components/host-widget/template.hbs index 087680bf8..fb55bacde 100644 --- a/app/pods/components/host-widget/template.hbs +++ b/app/pods/components/host-widget/template.hbs @@ -1,5 +1,6 @@
+
{{model.displayName}}
{{resource-actions model=model choices=model.primaryActions big=true}}
diff --git a/app/pods/components/page-header/template.hbs b/app/pods/components/page-header/template.hbs index 29c9fc23b..0cb0ada2a 100644 --- a/app/pods/components/page-header/template.hbs +++ b/app/pods/components/page-header/template.hbs @@ -52,8 +52,6 @@ {{/each}} - -
  • {{#link-to "projects"}}Manage Projects{{/link-to}}
  • diff --git a/app/pods/container/controller.js b/app/pods/container/controller.js index cc48d582b..c4f93336e 100644 --- a/app/pods/container/controller.js +++ b/app/pods/container/controller.js @@ -98,7 +98,7 @@ var ContainerController = Cattle.TransitioningResourceController.extend({ choices.push(byName('purge')); } - choices.push({ tooltip: 'Details', icon: 'fa-chevron-right', action: 'detail', enabled: true }); + choices.push({ tooltip: 'Details', icon: 'fa-info-circle', action: 'detail', enabled: true }); return choices; function byName(name) { diff --git a/app/pods/host/controller.js b/app/pods/host/controller.js index 014105991..f1bbd4a3e 100644 --- a/app/pods/host/controller.js +++ b/app/pods/host/controller.js @@ -61,7 +61,7 @@ var HostController = Cattle.TransitioningResourceController.extend({ choices.push(byName('promptDelete')); } - choices.push({ tooltip: 'Details', icon: 'fa-chevron-right', action: 'detail', enabled: true }); + choices.push({ tooltip: 'Details', icon: 'fa-info', action: 'detail', enabled: true }); return choices; function byName(name) { diff --git a/app/pods/host/template.hbs b/app/pods/host/template.hbs index cf67f2404..9f9e2c005 100644 --- a/app/pods/host/template.hbs +++ b/app/pods/host/template.hbs @@ -41,7 +41,10 @@
    - +
    +
    {{#link-to "containers.new" (query-params hostId=model.id) class="btn btn-primary btn-sm"}} Add a Container{{/link-to}}
    + +
    {{#each col in view.columns}}
    {{#each item in col itemController="container"}} diff --git a/app/pods/host/view.js b/app/pods/host/view.js index 829dddc1a..0ec28bdfc 100644 --- a/app/pods/host/view.js +++ b/app/pods/host/view.js @@ -53,13 +53,15 @@ export default Ember.View.extend({ var out = []; var i; + + for ( i = 0 ; i < columnCount ; i++ ) + { + out.push([]); + } + var instances = this.get('context.instances'); if ( instances ) { - for ( i = 0 ; i < columnCount ; i++ ) - { - out.push([]); - } for ( i = 0 ; i < instances.get('length') ; i++ ) { diff --git a/app/pods/project/controller.js b/app/pods/project/controller.js index 8e43ba35e..19e0e80ba 100644 --- a/app/pods/project/controller.js +++ b/app/pods/project/controller.js @@ -1,15 +1,44 @@ +import Ember from 'ember'; import Cattle from 'ui/utils/cattle'; +import C from 'ui/utils/constants'; var ProjectController = Cattle.TransitioningResourceController.extend({ + actions: { + edit: function() { + this.transitionToRoute('project.edit',this.get('id')); + }, + + delete: function() { + return this.delete(); + }, + + restore: function() { + return this.doAction('restore'); + }, + + purge: function() { + return this.doAction('purge'); + }, + + promptDelete: function() { + this.transitionToRoute('project.delete', this.get('id')); + }, + }, + + isDefault: Ember.computed.equal('externalIdType', C.PROJECT_TYPE_DEFAULT), + isUser: Ember.computed.equal('externalIdType', C.PROJECT_TYPE_USER), + isTeam: Ember.computed.equal('externalIdType', C.PROJECT_TYPE_TEAM), + isOrg: Ember.computed.equal('externalIdType', C.PROJECT_TYPE_ORG), + icon: function() { var icon = 'fa-question-circle'; switch ( this.get('externalIdType') ) { - case 'default': icon = 'fa-home'; break; - case 'project:github_user': icon = 'fa-github'; break; - case 'project:github_team': icon = 'fa-users'; break; - case 'project:github_org': icon = 'fa-building'; break; + case C.PROJECT_TYPE_DEFAULT: icon = 'fa-home'; break; + case C.PROJECT_TYPE_USER: icon = 'fa-github'; break; + case C.PROJECT_TYPE_TEAM: icon = 'fa-users'; break; + case C.PROJECT_TYPE_ORG: icon = 'fa-building'; break; } return icon; @@ -26,12 +55,61 @@ var ProjectController = Cattle.TransitioningResourceController.extend({ } }.property('icon','active'), + githubType: function() { + switch (this.get('externalIdType') ) + { + case C.PROJECT_TYPE_DEFAULT: return 'user'; + case C.PROJECT_TYPE_USER: return 'user'; + case C.PROJECT_TYPE_TEAM: return 'team'; + case C.PROJECT_TYPE_ORG: return 'org'; + } + }.property('externalIdType'), + + githubLogin: function() { + var type = this.get('externalIdType'); + + if ( type === C.PROJECT_TYPE_DEFAULT ) + { + return this.get('session.user'); + } + + return this.get('externalId'); + }.property('externalIdType', 'externalId'), + active: function() { return this.get('session.projectId') === this.get('id'); - }.property('session.projectId','id') + }.property('session.projectId','id'), + + availableActions: function() { + var a = this.get('actions'); + + var choices = [ +// { tooltip: 'Edit', icon: 'fa-edit', action: 'edit', enabled: !!a.update }, + { tooltip: 'Restore', icon: 'fa-ambulance', action: 'restore', enabled: !!a.restore }, + { tooltip: 'Delete', icon: 'fa-trash-o', action: 'promptDelete', enabled: !!a.remove, altAction: 'delete' }, + { tooltip: 'Purge', icon: 'fa-fire', action: 'purge', enabled: !!a.purge }, + ]; + + return choices; + }.property('actions.{update,restore,remove,purge}'), }); ProjectController.reopenClass({ + stateMap: { + 'activating': {icon: 'fa-ticket', color: 'text-danger'}, + 'active': {icon: 'fa-circle-o', color: 'text-info'}, + 'deactivating': {icon: 'fa-adjust', color: 'text-danger'}, + 'inactive': {icon: 'fa-stop', color: 'text-danger'}, + 'purged': {icon: 'fa-fire', color: 'text-danger'}, + 'purging': {icon: 'fa-fire', color: 'text-danger'}, + 'registering': {icon: 'fa-ticket', color: 'text-danger'}, + 'removed': {icon: 'fa-trash', color: 'text-danger'}, + 'removing': {icon: 'fa-trash', color: 'text-danger'}, + 'requested': {icon: 'fa-ticket', color: 'text-danger'}, + 'restoring': {icon: 'fa-trash', color: 'text-danger'}, + 'updating-active': {icon: 'fa-circle-o', color: 'text-info'}, + 'updating-inactive':{icon: 'fa-warning', color: 'text-danger'}, + } }); export default ProjectController; diff --git a/app/pods/project/delete/route.js b/app/pods/project/delete/route.js new file mode 100644 index 000000000..363fb540c --- /dev/null +++ b/app/pods/project/delete/route.js @@ -0,0 +1,22 @@ +import OverlayRoute from 'ui/pods/overlay/route'; + +export default OverlayRoute.extend({ + renderTemplate: function() { + this.render('confirmDelete', { + into: 'application', + outlet: 'overlay', + controller: 'project' + }); + }, + + actions: { + confirm: function() { + this.controllerFor('project').send('delete'); + this.send('goToPrevious'); + }, + + cancel: function() { + this.send('goToPrevious'); + } + } +}); diff --git a/app/pods/settings/projects/new/route.js b/app/pods/project/edit/route.js similarity index 100% rename from app/pods/settings/projects/new/route.js rename to app/pods/project/edit/route.js diff --git a/app/pods/settings/projects/new/template.hbs b/app/pods/project/edit/template.hbs similarity index 100% rename from app/pods/settings/projects/new/template.hbs rename to app/pods/project/edit/template.hbs diff --git a/app/pods/project/model.js b/app/pods/project/model.js index eac325dc5..3f862c8f4 100644 --- a/app/pods/project/model.js +++ b/app/pods/project/model.js @@ -1,4 +1,9 @@ import Cattle from 'ui/utils/cattle'; export default Cattle.TransitioningResource.extend({ + type: 'project', + name: null, + description: null, + externalId: null, + externalIdType: null }); diff --git a/app/pods/settings/projects/route.js b/app/pods/project/route.js similarity index 100% rename from app/pods/settings/projects/route.js rename to app/pods/project/route.js diff --git a/app/pods/settings/projects/template.hbs b/app/pods/project/template.hbs similarity index 100% rename from app/pods/settings/projects/template.hbs rename to app/pods/project/template.hbs diff --git a/app/pods/projects/new/controller.js b/app/pods/projects/new/controller.js new file mode 100644 index 000000000..7fcb8729b --- /dev/null +++ b/app/pods/projects/new/controller.js @@ -0,0 +1,59 @@ +import Ember from 'ember'; +import C from 'ui/utils/constants'; +import Cattle from 'ui/utils/cattle'; + +export default Ember.ObjectController.extend(Cattle.NewOrEditMixin, { + actions: { + selectOwner: function(type,login) { + this.beginPropertyChanges(); + this.set('externalId', login); + this.set('externalIdType', type); + this.endPropertyChanges(); + }, + }, + + typeUser: C.PROJECT_TYPE_USER, + typeTeam: C.PROJECT_TYPE_TEAM, + typeOrg: C.PROJECT_TYPE_ORG, + + ownerChoices: function() { + var out = []; + var externalIdType = this.get('externalIdType'); + var externalId = this.get('externalId'); + + out.pushObject({ + type: C.PROJECT_TYPE_USER, + githubType: 'user', + login: this.get('session.user'), + active: (externalIdType === C.PROJECT_TYPE_USER && externalId === this.get('session.user')), + }); + + /* @TODO fix team uniqueness, https://github.com/rancherio/cattle/issues/215 + out.pushObjects(this.get('session.teams').map(function(team) { + return { + type: C.PROJECT_TYPE_TEAM, + githubType: 'team', + login: team.name, + active: (externalIdType === C.PROJECT_TYPE_TEAM && externalId === team.name), + }; + })); + */ + + out.pushObjects(this.get('session.orgs').map(function(org) { + return { + type: C.PROJECT_TYPE_ORG, + githubType: 'org', + login: org, + active: (externalIdType === C.PROJECT_TYPE_ORG && externalId === org), + }; + })); + + return out; + }.property('session.user','session.teams.@each.name','session.orgs.[]','externalId','externalIdType'), + + doneSaving: function() { + var out = this._super(); + this.send('goToPrevious'); + return out; + }, +}); diff --git a/app/pods/projects/new/route.js b/app/pods/projects/new/route.js new file mode 100644 index 000000000..859cc53a9 --- /dev/null +++ b/app/pods/projects/new/route.js @@ -0,0 +1,29 @@ +import OverlayRoute from 'ui/pods/overlay/route'; + +export default OverlayRoute.extend({ + actions: { + cancel: function() { + this.send('goToPrevious'); + }, + }, + + model: function(/*params, transition*/) { + var model = this.get('store').createRecord({ + type: 'project', + externalIdType: 'project:github_user', + externalId: this.get('session.user'), + }); + + return model; + }, + + setupController: function(controller,model) { + this._super(); + controller.set('model', model); + controller.set('editing',false); + }, + + renderTemplate: function() { + this.render('projects/new', {into: 'application', outlet: 'overlay'}); + }, +}); diff --git a/app/pods/projects/new/template.hbs b/app/pods/projects/new/template.hbs new file mode 100644 index 000000000..6d52dd748 --- /dev/null +++ b/app/pods/projects/new/template.hbs @@ -0,0 +1,35 @@ +

    Create Project

    + +{{#if error}} +
    + +

    {{error}}

    +
    +{{/if}} + +
    +
    +
    + + {{input id="name" type="text" value=name classNames="form-control" placeholder="e.g. app01"}} +
    +
    + + {{textarea id="description" value=description classNames="form-control no-resize" rows="3" placeholder="e.g. It serves the webs"}} +
    +
    + +
    + +
    + {{#each choice in ownerChoices}} + + {{github-block link=false type=choice.githubType login=choice.login}} + + {{/each}} +
    +
    +
    + +{{partial "save-cancel"}} +{{partial "overlay-close"}} diff --git a/app/pods/projects/new/view.js b/app/pods/projects/new/view.js new file mode 100644 index 000000000..d07c695b6 --- /dev/null +++ b/app/pods/projects/new/view.js @@ -0,0 +1,13 @@ +import Overlay from 'ui/pods/overlay/view'; + +export default Overlay.extend({ + actions: { + overlayClose: function() { + this.get('controller').send('cancel'); + }, + + overlayEnter: function() { + this.get('controller').send('save'); + }, + }, +}); diff --git a/app/pods/projects/route.js b/app/pods/projects/route.js index 373b36dec..5e38fdfbf 100644 --- a/app/pods/projects/route.js +++ b/app/pods/projects/route.js @@ -1,8 +1,19 @@ +import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route'; import Ember from 'ember'; -export default Ember.Route.extend({ +export default Ember.Route.extend(AuthenticatedRouteMixin, { renderTemplate: function() { - this.send('setPageName','Projects'); this._super(); + this.send('setPageName','Manage Projects'); }, + + model: function() { + return this.get('store').findAll('project'); + }, + + actions: { + newProject: function() { + this.transitionTo('projects.new'); + } + } }); diff --git a/app/pods/projects/template.hbs b/app/pods/projects/template.hbs index c24cd6895..a070317af 100644 --- a/app/pods/projects/template.hbs +++ b/app/pods/projects/template.hbs @@ -1 +1,49 @@ -{{outlet}} +
    + {{#if error}} +
    + +

    {{error}}

    +
    + {{/if}} + +
    +

    Rancher supports grouping resources into multiple projects. Each project is owned by a GitHub user{{!, team,}} or organization, and has its own hosts and other resources.

    +

    For example, you might create separate "dev", "test", and "production" projects to keep logical environments isolated from each other, and restrict the "production" project to a small team of people.

    +

    Note: You cannot currently delete projects. (The cloud only scales up)

    +
    + +
    +
    +
    + +
    +
    + + + + + + + + {{#each p in arrangedContent itemController="project"}} + + + + + + + {{else}} + + {{/each}} +
    StateName
    Description
    Owner 
    + {{p.displayState}} + + {{#if p.name}}{{p.displayName}}{{else}}No name{{/if}} +

    {{#if p.description}}{{p.description}}{{else}}No description{{/if}}

    +
    + {{github-block type=p.githubType login=p.githubLogin}} + + {{resource-actions model=p choices=p.availableActions}} +
    You don't have any Projects yet.
    +
    +
    diff --git a/app/router.js b/app/router.js index 3e4bdb45b..7b557c0c0 100644 --- a/app/router.js +++ b/app/router.js @@ -14,9 +14,15 @@ Router.map(function() { this.route('authenticated', { path: '/'}, function() { this.resource('settings', function() { this.route('auth'); - this.resource('projects', function() { - this.route('new'); - }); + }); + + this.resource('projects', { path: '/projects' }, function() { + this.route('new'); + }); + + this.resource("project", { path: '/projects/:project_id' }, function() { + this.route("edit"); + this.route("delete"); }); this.resource('hosts', { path: '/hosts'}, function() { diff --git a/app/styles/bootstrap-tweak.scss b/app/styles/bootstrap-tweak.scss index 9ef1bae75..35355fc68 100644 --- a/app/styles/bootstrap-tweak.scss +++ b/app/styles/bootstrap-tweak.scss @@ -134,3 +134,7 @@ HR { color: $brand-success; font-weight: bold; } + +.list-group-item.active .text-muted { + color: inherit; +} diff --git a/app/styles/github-avatar.scss b/app/styles/github-avatar.scss index cdb8248a3..d8a9d4765 100644 --- a/app/styles/github-avatar.scss +++ b/app/styles/github-avatar.scss @@ -42,5 +42,12 @@ height: 40px; border: 1px solid #aaa; border-radius: 3px; + line-height: 40px; + text-align: center; + + .fa { + line-height: 36px; + color: #333; + } } } diff --git a/app/styles/host.scss b/app/styles/host.scss index 07eafbd2b..73589d894 100644 --- a/app/styles/host.scss +++ b/app/styles/host.scss @@ -11,7 +11,7 @@ $instance_action: #ededed; .hosts { overflow: visible; - white-space: no-wrap; + white-space: nowrap; } .host-zone { @@ -60,7 +60,6 @@ $instance_action: #ededed; border: 1px solid $host_border; border-top-width: 2px; border-radius: 5px; - min-height: 140px; .host-header { display: block; @@ -106,7 +105,7 @@ $instance_action: #ededed; right: 0; height: 74px; z-index: 1; - padding-top: 15px; + padding-top: 5px; text-align: center; } @@ -140,12 +139,11 @@ $instance_action: #ededed; .add-host { color: #5fcf80; background-color: transparent; - border-width: 2px; - border-style: dotted; + border: 2px dashed; text-align: center; cursor: pointer; cursor: hand; - + padding: 10px; &:hover { color: #999; diff --git a/app/styles/layout.scss b/app/styles/layout.scss index 3d30c66cd..ae4c6ec10 100644 --- a/app/styles/layout.scss +++ b/app/styles/layout.scss @@ -144,13 +144,13 @@ HEADER { background-color: $body_bg; .full-height { - line-height: $header_height; + line-height: $header_height - 1; } H2 { font-weight: normal; margin: 0; - line-height: $header_height; + line-height: $header_height - 1; color: $header_text; padding-left: 20px; } @@ -260,7 +260,7 @@ SECTION { font-family: Consolas, "Andale Mono", "Lucida Console", Monaco, "Courier New", Courier, monospace; } -SECTION .well > LABEL, +SECTION .well LABEL, .graphs LABEL { font-weight: normal; color: #da8456; diff --git a/app/utils/constants.js b/app/utils/constants.js index d548cba70..3bbe06405 100644 --- a/app/utils/constants.js +++ b/app/utils/constants.js @@ -10,6 +10,10 @@ export default { PROJECT_HEADER: 'x-api-project-id', PROJECT_SESSION_KEY: 'projectId', + PROJECT_TYPE_DEFAULT: 'default', + PROJECT_TYPE_USER: 'project:github_user', + PROJECT_TYPE_TEAM: 'project:github_team', + PROJECT_TYPE_ORG: 'project:github_org', NO_CHALLENGE_HEADER: 'x-api-no-challenge', NO_CHALLENGE_VALUE: 'true', diff --git a/scripts/build-static b/scripts/build-static index f9e983ed7..3d0323a60 100755 --- a/scripts/build-static +++ b/scripts/build-static @@ -39,7 +39,7 @@ EOF # Why are you trying to do a build when there are uncommitted changes? if [[ `git status --porcelain` ]]; then echo "There are uncommited changes. Please check the number and try again." -# exit 1; + exit 1; fi echo "Environment Variables:" diff --git a/tests/unit/pods/settings/projects/new/route-test.js b/tests/unit/pods/project/edit/route-test.js similarity index 76% rename from tests/unit/pods/settings/projects/new/route-test.js rename to tests/unit/pods/project/edit/route-test.js index 43c436fbb..57835cb0c 100644 --- a/tests/unit/pods/settings/projects/new/route-test.js +++ b/tests/unit/pods/project/edit/route-test.js @@ -3,7 +3,7 @@ import { test } from 'ember-qunit'; -moduleFor('route:settings/projects/new', 'SettingsProjectsNewRoute', { +moduleFor('route:project/edit', 'ProjectEditRoute', { // Specify the other units that are required for this test. // needs: ['controller:foo'] }); diff --git a/tests/unit/pods/project/route-test.js b/tests/unit/pods/project/route-test.js new file mode 100644 index 000000000..4ec77f2f4 --- /dev/null +++ b/tests/unit/pods/project/route-test.js @@ -0,0 +1,14 @@ +import { + moduleFor, + test +} from 'ember-qunit'; + +moduleFor('route:project', 'ProjectRoute', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function() { + var route = this.subject(); + ok(route); +});