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 @@ +
+

{{t 'projectsPage.header'}}

+
+ +
+ {{#link-to "authenticated.projects.new" class="btn bg-primary btn-sm icon-btn"}} + + + + {{t 'projectsPage.addProject'}} + {{/link-to}} +
+
+{{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 @@ +
+
+ {{#if editing}} +

{{t 'projectsPage.editProject'}}

+ {{else}} +

{{t 'projectsPage.addProject'}}

+ {{/if}} +
+
+ +
+ {{form-name-description + name=model.project.name + nameLabel="projectsPage.new.form.name.label" + nameRequired=true + nameDisabled=editing + namePlaceholder="projectsPage.new.form.name.placeholder" + colClass="col span-7" + descriptionShown=false + }} +
+ +
+
+
+ +
+
+
+ + + + + + + + + + + + {{#each memberArray as |member|}} + {{project-member-row member=member roles=model.roles remove="removeMember"}} + {{/each}} + +
{{t 'projectsPage.new.form.members.kind.label'}}{{t 'projectsPage.new.form.members.name.label'}}{{t 'projectsPage.new.form.members.role.label'}} 
+
+
+ + + + + +
+
+ +{{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