Implemented CRUD for project

This commit is contained in:
loganhz 2017-11-26 11:48:50 +08:00
parent e761fefd8e
commit bb9e744ccf
20 changed files with 379 additions and 127 deletions

View File

@ -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'),
});
},
});

View File

@ -0,0 +1 @@
{{new-edit-project model=model editing=true}}

View File

@ -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
};
});
},
});

View File

@ -0,0 +1,14 @@
<section class="header clearfix">
<h1 class="pull-left">{{t 'projectsPage.header'}}</h1>
<div class="vertical-middle"></div>
<div class="right-buttons">
{{#link-to "authenticated.projects.new" class="btn bg-primary btn-sm icon-btn"}}
<span class="darken">
<i class="icon icon-folder"></i>
</span>
<span>{{t 'projectsPage.addProject'}}</span>
{{/link-to}}
</div>
</section>
{{project-table model=model.projects}}

View File

@ -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'),
});
},
});

View File

@ -0,0 +1 @@
{{new-edit-project model=model editing=false}}

View File

@ -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') )
{

View File

@ -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: '/'});

View File

@ -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();
},
});

View File

@ -0,0 +1,66 @@
<section class="header clearfix">
<div class="pull-left">
{{#if editing}}
<h1>{{t 'projectsPage.editProject'}}</h1>
{{else}}
<h1>{{t 'projectsPage.addProject'}}</h1>
{{/if}}
</div>
</section>
<section class="horizontal-form container-fluid">
{{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
}}
</section>
<section class="horizontal-form container-fluid">
<div class="row">
<div class="col span-3">
<label class="pb-5 acc-label">{{t 'projectsPage.new.form.members.labelText'}}</label>
</div>
</div>
<div class="pl-10 pr-10">
<table class="table fixed no-lines">
<thead>
<tr>
<th width="150">{{t 'projectsPage.new.form.members.kind.label'}}</th>
<th>{{t 'projectsPage.new.form.members.name.label'}}</th>
<th>{{t 'projectsPage.new.form.members.role.label'}}</th>
<th width="10">&nbsp;</th>
<th width="40"></th>
</tr>
</thead>
<tbody>
{{#each memberArray as |member|}}
{{project-member-row member=member roles=model.roles remove="removeMember"}}
{{/each}}
</tbody>
</table>
</div>
<div class="mt-5">
<button class="btn bg-link icon-btn p-0" {{action "addMember" "User"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span>
<span>{{t 'projectsPage.new.form.members.addUser'}}</span>
</button>
<button class="btn bg-link icon-btn p-0" {{action "addMember" "Group"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span>
<span>{{t 'projectsPage.new.form.members.addGroup'}}</span>
</button>
<button class="btn bg-link icon-btn p-0" {{action "addMember" "ServiceAccount"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span>
<span>{{t 'projectsPage.new.form.members.addServiceAccount'}}</span>
</button>
</div>
</section>
{{top-errors errors=errors}}
{{save-cancel createLabel=(if editing 'projectsPage.saveEdit' 'projectsPage.saveNew') save="save" cancel="cancel"}}

View File

@ -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')
});

View File

@ -0,0 +1,15 @@
<td class="pr-20">
{{t kind}}
</td>
<td class="pr-20">
{{input type="text" value=member.subjectName classNames="form-control"}}
</td>
<td class="pr-20">
{{searchable-select content=choices value=member.projectRoleTemplateId}}
</td>
<td>&nbsp;</td>
<div class="input-group-btn">
<button class="btn bg-primary btn-sm" {{action "remove"}}>
<i class="icon icon-minus" />
</button>
</div>

View File

@ -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);
}
},
});

View File

@ -1,36 +1,12 @@
{{#if bulkActions}}
<td valign="middle" class="row-check" style="padding-top: 2px;">
{{check-box nodeId=model.id}}
</td>
{{/if}}
<td data-title="{{dt.state}}" class="state">
{{badge-state model=model}}
<td valign="middle" class="row-check" style="padding-top: 2px;">
&nbsp;
</td>
{{#if showCluster}}
<td data-title="{{dt.cluster}}">
<a href="{{href-to 'authenticated.clusters.cluster.index' model.cluster.id}}">{{model.cluster.displayName}}</a>
</td>
{{/if}}
<td data-title="{{dt.name}}">
<a href="{{href-to 'authenticated.project.index' model.id}}" {{action "switchTo" model.id}}>{{model.displayName}}</a>
<td data-title="{{t 'projects.table.project.label'}}:" class="clip">
{{#link-to "authenticated.projects.edit" model.id }} {{model.name}} {{/link-to}}
</td>
<td colspan="3" class="text-center text-muted">
Counts Coming Soon
<td data-title="{{t 'projects.table.created.label'}}:">
{{date-calendar model.created}}
</td>
<!--
<td data-title="{{dt.stacks}}" class="text-center">
{{model.numStacks}}
</td>
<td data-title="{{dt.services}}" class="text-center">
{{model.numServices}}
</td>
<td data-title="{{dt.containers}}" class="text-center">
{{model.numContainers}}
</td>
-->
<td data-title="{{dt.default}}" class="text-center">
{{#if model.isDefault}}<i class="icon icon-star-fill"></i>{{else}}<span class="text-muted">&ndash;</span>{{/if}}
</td>
<td data-title="{{dt.actions}} "class="actions">
<td data-title="{{t 'generic.actions'}}:" class="actions">
{{action-menu model=model}}
</td>
</td>

View File

@ -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',
});

View File

@ -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")}}
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'clusterRow.noMatch'}}</td></tr>
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'projectsPage.table.noMatch'}}</td></tr>
{{else if (eq kind "norows")}}
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'clusterRow.noData'}}</td></tr>
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'projectsPage.table.noData'}}</td></tr>
{{/if}}
{{/sortable-table}}

View File

@ -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) {

View File

@ -0,0 +1 @@
export { default } from 'shared/components/new-edit-project/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/project-member-row/component';

View File

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