diff --git a/app/authenticated/projects/edit/route.js b/app/authenticated/projects/edit/route.js
new file mode 100644
index 000000000..e19b32cf9
--- /dev/null
+++ b/app/authenticated/projects/edit/route.js
@@ -0,0 +1,15 @@
+import { hash } from 'rsvp';
+import { inject as service } from '@ember/service';
+import Route from '@ember/routing/route';
+
+export default Route.extend({
+ authzStore: service('authz-store'),
+ model: function (params) {
+ return hash({
+ project: this.get('authzStore').find('project', params.project_id),
+ projectRoleTemplateBindings: this.get('authzStore').findAll('projectRoleTemplateBinding', null, { filter: { projectId: params.project_id } }),
+ projects: this.get('authzStore').findAll('project'),
+ roles: this.get('authzStore').findAll('projectRoleTemplate'),
+ });
+ },
+});
\ No newline at end of file
diff --git a/app/authenticated/projects/edit/template.hbs b/app/authenticated/projects/edit/template.hbs
new file mode 100644
index 000000000..b209c1ba9
--- /dev/null
+++ b/app/authenticated/projects/edit/template.hbs
@@ -0,0 +1 @@
+{{new-edit-project model=model editing=true}}
diff --git a/app/authenticated/projects/index/route.js b/app/authenticated/projects/index/route.js
new file mode 100644
index 000000000..f97c9bc74
--- /dev/null
+++ b/app/authenticated/projects/index/route.js
@@ -0,0 +1,13 @@
+import { inject as service } from '@ember/service';
+import Route from '@ember/routing/route';
+
+export default Route.extend({
+ authzStore: service('authz-store'),
+ model: function () {
+ return this.get('authzStore').findAll('project').then(projects => {
+ return {
+ projects
+ };
+ });
+ },
+});
diff --git a/app/authenticated/projects/index/template.hbs b/app/authenticated/projects/index/template.hbs
new file mode 100644
index 000000000..9152b864e
--- /dev/null
+++ b/app/authenticated/projects/index/template.hbs
@@ -0,0 +1,14 @@
+
+{{project-table model=model.projects}}
\ No newline at end of file
diff --git a/app/authenticated/projects/new/route.js b/app/authenticated/projects/new/route.js
new file mode 100644
index 000000000..008b64218
--- /dev/null
+++ b/app/authenticated/projects/new/route.js
@@ -0,0 +1,25 @@
+import { hash } from 'rsvp';
+import { inject as service } from '@ember/service';
+import Route from '@ember/routing/route';
+
+export default Route.extend({
+ authzStore: service('authz-store'),
+ model: function () {
+ const project = this.get('authzStore').createRecord({
+ type: `project`,
+ name: '',
+ });
+ const projectRoleTemplateBindings = [{
+ subjectKind: 'User',
+ subjectName: '',
+ projectRoleTemplateId: '',
+ projectId: '',
+ }];
+ return hash({
+ project,
+ projectRoleTemplateBindings,
+ projects: this.get('authzStore').findAll('project'),
+ roles: this.get('authzStore').findAll('projectRoleTemplate'),
+ });
+ },
+});
\ No newline at end of file
diff --git a/app/authenticated/projects/new/template.hbs b/app/authenticated/projects/new/template.hbs
new file mode 100644
index 000000000..8b118a8d3
--- /dev/null
+++ b/app/authenticated/projects/new/template.hbs
@@ -0,0 +1 @@
+{{new-edit-project model=model editing=false}}
diff --git a/app/models/project.js b/app/models/project.js
index 71c144137..eacfe9500 100644
--- a/app/models/project.js
+++ b/app/models/project.js
@@ -32,17 +32,7 @@ var Project = Resource.extend(PolledResource, {
actions: {
edit: function() {
- this.get('router').transitionTo('authenticated.clusters.project', this.get('id'));
- },
-
- delete: function() {
- return this.delete().then(() => {
- // If you're in the project that was deleted, go back to the default project
- if ( this.get('active') )
- {
- window.location.href = window.location.href;
- }
- });
+ this.get('router').transitionTo('authenticated.projects.edit', this.get('id'));
},
activate: function() {
@@ -85,14 +75,9 @@ var Project = Resource.extend(PolledResource, {
let l = this.get('links');
var choices = [
- { label: 'action.setDefault', icon: 'icon icon-star-fill', action: 'setAsDefault', enabled: this.get('canSetDefault')},
+ { label: 'action.edit', icon: 'icon icon-edit', action: 'edit', enabled: true},
{ divider: true },
- { label: 'action.edit', icon: 'icon icon-edit', action: 'edit', enabled: !!l.update },
- { divider: true },
- { label: 'action.activate', icon: 'icon icon-play', action: 'activate', enabled: !!a.activate, bulkable: true},
- { label: 'action.deactivate', icon: 'icon icon-pause', action: 'promptStop', enabled: !!a.deactivate, altAction: 'deactivate', bulkable: true},
- { divider: true },
- { label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', enabled: !!l.remove, altAction: 'delete', bulkable: true },
+ { label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', enabled: true, altAction: 'delete', bulkable: true },
{ divider: true },
{ label: 'action.viewInApi', icon: 'icon icon-external-link',action: 'goToApi', enabled: true },
];
@@ -100,6 +85,15 @@ var Project = Resource.extend(PolledResource, {
return choices;
}),
+ delete: function (/*arguments*/) {
+ var promise = this._super.apply(this, arguments);
+ return promise.then(() => {
+ this.set('state', 'removed');
+ }).catch((err) => {
+ this.get('growl').fromError('Error deleting', err);
+ });
+ },
+
icon: computed('active', function() {
if ( this.get('active') )
{
diff --git a/app/router.js b/app/router.js
index 5dc335f34..7de54ce29 100644
--- a/app/router.js
+++ b/app/router.js
@@ -37,6 +37,12 @@ Router.map(function() {
this.route('prefs');
+ this.route('projects', {path: '/projects'}, function() {
+ this.route('index', {path: '/'});
+ this.route('edit', {path: '/:project_id'});
+ this.route('new', {path: '/add'});
+ });
+
// Clusters
this.route('clusters', {path: '/clusters'}, function() {
this.route('index', {path: '/'});
diff --git a/lib/shared/addon/components/new-edit-project/component.js b/lib/shared/addon/components/new-edit-project/component.js
new file mode 100644
index 000000000..8a0d4146f
--- /dev/null
+++ b/lib/shared/addon/components/new-edit-project/component.js
@@ -0,0 +1,113 @@
+import { alias } from '@ember/object/computed';
+import { inject as service } from '@ember/service';
+import { reject, all as PromiseAll } from 'rsvp';
+import Component from '@ember/component';
+import NewOrEdit from 'ui/mixins/new-or-edit';
+import layout from './template';
+
+export default Component.extend(NewOrEdit, {
+ layout,
+ intl: service(),
+ router: service(),
+ authzStore: service('authz-store'),
+ model: null,
+
+ primaryResource: alias('model.project'),
+ memberArray: alias('model.projectRoleTemplateBindings'),
+
+ actions: {
+ cancel() {
+ this.goBack();
+ },
+ addMember(kind) {
+ this.get('memberArray').pushObject({
+ subjectKind: kind,
+ subjectName: '',
+ projectRoleTemplateId: '',
+ projectId: '',
+ });
+ },
+ removeMember(obj) {
+ this.get('memberArray').removeObject(obj);
+ },
+ },
+
+ goBack: function () {
+ this.get('router').transitionTo('/projects');
+ },
+
+ doesNameExist() {
+ const project = this.get('primaryResource');
+ const currentProjects = this.get('model.projects');
+
+ if (currentProjects.findBy('name', project.get('name'))) {
+ return true;
+ }
+
+ return false;
+ },
+
+ doseMemberNameInvalid() {
+ const members = this.get('memberArray');
+ return members.any(r => r.subjectName.length === 0);
+ },
+
+ doseMemberRoleInvalid() {
+ const members = this.get('memberArray');
+ return members.any(r => r.projectRoleTemplateId.length === 0);
+ },
+
+ validate: function () {
+ var errors = this.get('errors', errors) || [];
+
+ if ((this.get('model.project.name') || '').trim().length === 0) {
+ errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.nameReq'));
+ }
+
+ if (!this.get('editing') && this.doesNameExist()) {
+ errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.nameInExists'));
+ }
+
+ if (this.doseMemberNameInvalid()) {
+ errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.memberNameReq'));
+ }
+
+ if (this.doseMemberRoleInvalid()) {
+ errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.memberRoleReq'));
+ }
+
+ if (errors.length) {
+ this.set('errors', errors.uniq());
+ return false;
+ }
+ else {
+ this.set('errors', null);
+ }
+
+ return true;
+ },
+
+ doSave() {
+ return this._super.apply(this, arguments).then((project) => {
+ const projectId = project.id;
+ const members = this.get('memberArray');
+ const promises = [];
+ members.forEach(member => {
+ member.projectId = projectId;
+ const promise = this.get('authzStore').rawRequest({
+ url: 'projectroletemplatebinding',
+ method: 'POST',
+ data: member,
+ });
+ promises.push(promise);
+ });
+ return PromiseAll(promises).catch((error) => {
+ return reject(error.body.message);
+ });
+ });
+ },
+
+ doneSaving() {
+ this.goBack();
+ },
+});
\ No newline at end of file
diff --git a/lib/shared/addon/components/new-edit-project/template.hbs b/lib/shared/addon/components/new-edit-project/template.hbs
new file mode 100644
index 000000000..cb0b6c3e1
--- /dev/null
+++ b/lib/shared/addon/components/new-edit-project/template.hbs
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+{{top-errors errors=errors}}
+{{save-cancel createLabel=(if editing 'projectsPage.saveEdit' 'projectsPage.saveNew') save="save" cancel="cancel"}}
\ No newline at end of file
diff --git a/lib/shared/addon/components/project-member-row/component.js b/lib/shared/addon/components/project-member-row/component.js
new file mode 100644
index 000000000..a3e04393e
--- /dev/null
+++ b/lib/shared/addon/components/project-member-row/component.js
@@ -0,0 +1,31 @@
+import Component from '@ember/component';
+import layout from './template';
+
+export default Component.extend({
+ layout,
+ member: null,
+ roles: null,
+
+ tagName: 'TR',
+ classNames: 'main-row',
+
+ actions: {
+ remove: function () {
+ this.sendAction('remove', this.get('member'));
+ }
+ },
+
+ init: function () {
+ this._super(...arguments);
+ this.set('choices', this.get('roles').map(role => {
+ return {
+ label: role.name,
+ value: role.id,
+ };
+ }));
+ },
+
+ kind: function() {
+ return `projectsPage.new.form.members.${this.get('member.subjectKind').toLowerCase()}`
+ }.property('member.subjectKind')
+});
diff --git a/lib/shared/addon/components/project-member-row/template.hbs b/lib/shared/addon/components/project-member-row/template.hbs
new file mode 100644
index 000000000..e989cf482
--- /dev/null
+++ b/lib/shared/addon/components/project-member-row/template.hbs
@@ -0,0 +1,15 @@
+
+ {{t kind}}
+
+
+ {{input type="text" value=member.subjectName classNames="form-control"}}
+
+
+ {{searchable-select content=choices value=member.projectRoleTemplateId}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/shared/addon/components/project-row/component.js b/lib/shared/addon/components/project-row/component.js
index af50ce6c6..fa7671e9d 100644
--- a/lib/shared/addon/components/project-row/component.js
+++ b/lib/shared/addon/components/project-row/component.js
@@ -1,18 +1,8 @@
import Component from '@ember/component';
import layout from './template';
-import { inject as service } from '@ember/service'
export default Component.extend({
layout,
model: null,
tagName: 'TR',
- showCluster: false,
- scope: service(),
-
- actions: {
- switchTo(id) {
- // @TODO bad
- window.lc('authenticated').send('switchProject', id);
- }
- },
});
diff --git a/lib/shared/addon/components/project-row/template.hbs b/lib/shared/addon/components/project-row/template.hbs
index e070427df..a55acc39e 100644
--- a/lib/shared/addon/components/project-row/template.hbs
+++ b/lib/shared/addon/components/project-row/template.hbs
@@ -1,36 +1,12 @@
-{{#if bulkActions}}
-
- {{check-box nodeId=model.id}}
-
-{{/if}}
-
- {{badge-state model=model}}
+
+
-{{#if showCluster}}
-
- {{model.cluster.displayName}}
-
-{{/if}}
-
- {{model.displayName}}
+
+ {{#link-to "authenticated.projects.edit" model.id }} {{model.name}} {{/link-to}}
-
- Counts Coming Soon
+
+ {{date-calendar model.created}}
-
-
- {{#if model.isDefault}} {{else}}– {{/if}}
-
-
+
{{action-menu model=model}}
-
+
\ No newline at end of file
diff --git a/lib/shared/addon/components/project-table/component.js b/lib/shared/addon/components/project-table/component.js
index 25fbf6986..cc0f61a77 100644
--- a/lib/shared/addon/components/project-table/component.js
+++ b/lib/shared/addon/components/project-table/component.js
@@ -1,70 +1,21 @@
-import { computed } from '@ember/object';
import Component from '@ember/component';
import layout from './template';
-const headersWithCluster = [
- {
- name: 'state',
- sort: ['sortState','name','id'],
- translationKey: 'generic.state',
- width: 125,
- },
- {
- name: 'cluster',
- sort: ['cluster.displayName','displayName','id'],
- translationKey: 'clustersPage.cluster.label',
- searchField: ['cluster.displayName'],
- },
- {
+const headers = [{
name: 'name',
- sort: ['displayName','id'],
- translationKey: 'clustersPage.environment.label',
- searchField: ['displayName'],
- },
- {
- name: 'stacks',
- sort: ['numStacks','name','id'],
- translationKey: 'generic.stacks',
- width: 100,
- classNames: 'text-center',
- },
- {
- name: 'services',
- sort: ['numServices','name','id'],
- translationKey: 'generic.services',
- width: 100,
- classNames: 'text-center',
- },
- {
- name: 'containers',
- sort: ['numContainers','name','id'],
- translationKey: 'generic.containers',
- width: 120,
- classNames: 'text-center',
- },
- {
- name: 'default',
- sort: false,
- translationKey: 'clusterRow.loginDefault',
- width: 60,
- classNames: 'text-center',
+ sort: ['name'],
+ translationKey: 'projectsPage.table.header.project.label',
+ },{
+ name: 'created',
+ sort: ['created'],
+ translationKey: 'projectsPage.table.header.created.label',
+ width: '125',
},
];
-const headersWithoutCluster = headersWithCluster.filter(x => x.name !== 'cluster');
-
export default Component.extend({
layout,
tagName: '',
- showCluster: false,
- bulkActions: true,
- search: true,
-
- headers: computed('showCluster', function() {
- if ( this.get('showCluster') ) {
- return headersWithCluster;
- } else {
- return headersWithoutCluster;
- }
- }),
+ headers,
+ sortBy: 'name',
});
diff --git a/lib/shared/addon/components/project-table/template.hbs b/lib/shared/addon/components/project-table/template.hbs
index 7d672f7bb..f66ac87b6 100644
--- a/lib/shared/addon/components/project-table/template.hbs
+++ b/lib/shared/addon/components/project-table/template.hbs
@@ -1,22 +1,18 @@
{{#sortable-table
tableClassNames="bordered"
- bulkActions=bulkActions
paging=true
pagingLabel="pagination.project"
- search=search
- sortBy=sortBy
- descending=descending
headers=headers
body=model
+ sortBy=sortBy
fullRows=true
- stickyHeader=false
as |sortable kind p dt|
}}
{{#if (eq kind "row")}}
- {{project-row model=p dt=dt showCluster=showCluster search=search bulkActions=bulkActions}}
+ {{project-row model=p dt=dt}}
{{else if (eq kind "nomatch")}}
- {{t 'clusterRow.noMatch'}}
+ {{t 'projectsPage.table.noMatch'}}
{{else if (eq kind "norows")}}
- {{t 'clusterRow.noData'}}
+ {{t 'projectsPage.table.noData'}}
{{/if}}
{{/sortable-table}}
diff --git a/lib/shared/addon/utils/navigation-tree.js b/lib/shared/addon/utils/navigation-tree.js
index d8924ca99..40a7f274c 100644
--- a/lib/shared/addon/utils/navigation-tree.js
+++ b/lib/shared/addon/utils/navigation-tree.js
@@ -29,6 +29,10 @@ export const getClusterId = function() { return this.get('clusterId'); };
},
*/
const navTree = [
+ {
+ route: 'authenticated.projects.index',
+ localizedLabel: 'projectsPage.header'
+ }
];
export function addItem(opt) {
diff --git a/lib/shared/app/components/new-edit-project/component.js b/lib/shared/app/components/new-edit-project/component.js
new file mode 100644
index 000000000..506441072
--- /dev/null
+++ b/lib/shared/app/components/new-edit-project/component.js
@@ -0,0 +1 @@
+export { default } from 'shared/components/new-edit-project/component';
\ No newline at end of file
diff --git a/lib/shared/app/components/project-member-row/component.js b/lib/shared/app/components/project-member-row/component.js
new file mode 100644
index 000000000..27cc95184
--- /dev/null
+++ b/lib/shared/app/components/project-member-row/component.js
@@ -0,0 +1 @@
+export { default } from 'shared/components/project-member-row/component';
\ No newline at end of file
diff --git a/translations/en-us.yaml b/translations/en-us.yaml
index 450c2a6fc..713d81da4 100644
--- a/translations/en-us.yaml
+++ b/translations/en-us.yaml
@@ -497,6 +497,45 @@ certificatesPage:
description:
placeholder: e.g. EV cert for mydomain.com
+projectsPage:
+ header: Projects
+ addProject: Add Project
+ editProject: Edit Project
+ saveEdit: Edit
+ saveNew: Create
+ table:
+ noMatch: No projects match the current search.
+ noData: This cluster doesn't have any projects yet.
+ header:
+ project:
+ label: Project Name
+ created:
+ label: Created
+ new:
+ form:
+ name:
+ placeholder: e.g. lab
+ label: Project Name
+ members:
+ user: User
+ group: Group
+ serviceaccount: Service Account
+ labelText: Members
+ addUser: Add User
+ addGroup: Add Group
+ addServiceAccount: Add Service Account
+ kind:
+ label: Kind
+ name:
+ label: Name
+ role:
+ label: Role
+ errors:
+ nameReq: Name is requried.
+ nameInExists: Name is already exists. Please use a new project name.
+ memberNameReq: Name is requried for a member
+ memberRoleReq: Role is requried for a member
+
clustersPage:
header: Clusters
newCluster: Add Cluster